设计模式之单例模式(一)

目录

 

一. 单例模式概述

二. 单例模式常见的5种实现方式

1.饿汉式

2.懒汉式

3.静态内部类

4.枚举

5.双重检查

三. 单例模式再深入

懒汉式为什么不是线程安全的?该如何改进使其线程安全?

懒汉式与饿汉式场景记忆

如何选用单例模式?

面试技巧


一. 单例模式概述

特点:单例对象的类在整个系统中有且只有一个实例,并且只提供一个全局访问点。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

类型: 创建型模式

主要解决:一个全局使用的类频繁地创建与销毁。

关键代码:私有的构造函数、全局静态的实例化方法。

结构:

注意:

  • 1、单例类只能有一个实例。

  • 2、单例类必须自己创建自己的唯一实例。

  • 3、单例类必须给所有其他对象提供这一实例。

 

优点: 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

应用实例: 1、一个党只能有一个书记。 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

使用场景:

1、Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗?

2、Windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

3、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来,否则难以同步。

4、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

5、Spring中创建的Bean实例默认都是单例模式存在的。

6、应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

7、 Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

8、 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗

9、 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

10、 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

11、web开发中读取配置文件都适合使用单例模式。HttpApplication 也是单例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

12、要求生产唯一序列号

二. 单例模式常见的5种实现方式

1.饿汉式

public class EagerSingleton {
​
    //创建 EagerSingleton 的一个对象
    private static EagerSingleton instance = new EagerSingleton();
​
    //让构造函数为 private,这样该类就不会被实例化
    private EagerSingleton() {
    }
​
    //获取唯一可用的对象
    public static EagerSingleton getInstance() {
        return instance;
    }
​
    public void showMessage(){
        System.out.println("Hello World!");
    }
}

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。 缺点:类加载时就初始化,浪费内存。 它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

2.懒汉式

public class LazySingleton {
​
    private static LazySingleton lazySingleton;
​
    private LazySingleton(){
​
    }
​
    public static LazySingleton getInstantce(){
        //当使用该类的实例时,再去创建对象。懒加载但是不支持多线程
        if(null == lazySingleton){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
​
}

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

3.静态内部类

public class HolderSingleton {
​
    public static class SingletonHolder{
        //必须是final
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }
​
    private HolderSingleton(){
​
    }
​
    public static HolderSingleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

是否 Lazy 初始化:

是否多线程安全:

实现难度:一般

4.枚举

public enum EnumSingleton {
    INSTANCE;
​
    public void whateverMethod() {
    }
}

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。

5.双重检查

 

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多线程安全:

实现难度:较复杂

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。 getInstance() 的性能对应用程序很关键。

三. 单例模式再深入

懒汉式为什么不是线程安全的?该如何改进使其线程安全?

建一个线程类创建单例对象,并获取单例对象的hashCode值

public class SingletonThread extends Thread {
    @Override
    public void run() {
        System.out.println("对象的hashCode值为"+LazySingleton.getInstantce().hashCode());
    }
}
@Test
public void testLazySingleton() {
    for (int i = 0; i < 10; i++) {
        Thread thread = new SingletonThread();
        thread.run();
    }
}
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858
//对象的hashCode值为1394438858

可以看出懒汉式在没有并发的情况下是一个安全的单例模式,全局都只有一个单例模式的对象。但是在多线程的情况下

@Test
public void testLazySingleton() {
    for (int i = 0; i < 10; i++) {
        Thread thread = new SingletonThread();
        thread.start();
    }
}
//对象的hashCode值为913171578
//对象的hashCode值为913171578
//对象的hashCode值为1095766471
//对象的hashCode值为913171578
//对象的hashCode值为913171578
//对象的hashCode值为1563292559
//对象的hashCode值为913171578
//对象的hashCode值为913171578
//对象的hashCode值为1095766471
//对象的hashCode值为913171578

就可能创建出多个LazySingleton类的对象。所有说懒汉式单例模式在多线程并发的环境中是非线程安全的。那么再来看LazySingleton中的这段代码

 if(null == lazySingleton){
            lazySingleton = new LazySingleton();
        }

这里先判断了LazySingleton类是否已经有了该类的实例对象,如果没有实例对象的话再去创建对象。但是如果在并发的情况下,可能多个线程可能同时运行了这段代码,lazySingleton这时都为空,就会创建多个对象了,如测试类所示就会创建出多个。

懒汉式线程优化

public class SafeLazySingleton {
​
    private static SafeLazySingleton safeLazySingleton;
​
    private SafeLazySingleton(){
​
    }
​
    public static synchronized SafeLazySingleton getInstance(){
        if (null == safeLazySingleton){
            safeLazySingleton = new SafeLazySingleton();
        }
        return safeLazySingleton;
    }
}

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。 优点:第一次调用才初始化,避免内存浪费。 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。即使对象已经初始化了,也无法避免锁。

知道有这种优化方法就可以了,项目中不要用。

懒汉式与饿汉式场景记忆

懒汉式和饿汉式主要区别是:是否支持懒加载

用吃饭这个场景来比喻这两种实现方式:

懒汉式就是这个人比较懒,要吃饭的时候才去做饭。在代码中就是声明静态变量但是不初始化。

而饿汉式就好比一个人每次饿了的时候都要去做饭,久而久之饿怕了。所以不管饿不饿先做好了饭,饿了的时候直接拿饭来吃,无需再做饭。在代码中就是静态变量初始化。

如何选用单例模式?

看完了5种单例模式和1种变形,我们来总结下这6种单例模式

实现方式关键点资源浪费线程安全多线程环境的性能足够优化
饿汉式静态变量初始化
懒汉式懒加载-
懒汉式优化懒加载、同步
静态内部类在内部类里初始化,且final修饰
枚举枚举本质
双重检查懒加载、volatile、锁class

对于项目启动不会立即使用的对象,不应该采用饿汉式。在多线程的环境下,不应该使用懒汉式。虽然枚举单例模式中使用的不多,但是代码足够简介且性能优化,所以可以尝试使用这种单例模式。静态内部类和双重检查的表现也足够优秀,可以根据具体需求来选择使用哪种实现方式。

面试技巧

初中级面试者如果简历上有写设计模式的部分,单例模式会是经常被问到的模式。

曾经有一个面试官就和我说到过,单例模式如果只知道懒汉式和饿汉式这两种,那回答的这个问题会相当减分。一是这两种实现方式分别有自己的缺陷,再就是说明这个面试者并没有真正的去了解过单例。所以介绍单例模式的时候最少要说出三种实现方式。

如果让你实现一种单例模式,特别是上机题或者笔试题,最好的答案是双重检查。不是说双重检查相比其他实现方式来说有多么优秀,只是说通过这一个问题,可以引申出多个考验Java基础的问题。而通过引导面试官问这些问题,回答的又不错的话,那会比其他实现方式多加好多印象分。

下一篇,我会继续整理单例模式,会根据以下这几个问题来继续分析双重检查这种实现方式,有兴趣的可以共同交流一下。

1.如何破坏单例?

2.private static volatile DoubleCheckSingleton instance; private static volatile 这三个关键词分别有什么作用?

3.synchronized (DoubleCheckSingleton.class); 为什么锁的是.class,与懒汉式的优化相比有什么好处?

4. if(null == instance) 两个非空判断分别有怎么作用?

 

引用: http://www.runoob.com/design-pattern/singleton-pattern.html

https://www.cnblogs.com/gmq-sh/p/5948379.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值