单例模式
定义:单例对象的类必须保证只有一个实例存在。
分类:
对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:
- 懒汉式:指全局的单例实例在第一次被使用时构建。
- 饿汉式:指全局的单例实例在类装载时构建。
日常我们使用的较多的应该是懒汉式的单例,因为按需加载才能做到资源的最大化利用。
单例模式的实现
懒汉式单例
Version 1.0
|
|
上述代码所做的事情就是每次获取instance之前先进行判断,如果instance为空就new一个出来,否则就直接返回已存在的instance。
显然,这种做法是在单线程的条件下才能正常工作的单例模式。如果工作在多线程环境,多个线程有可能同时进入 if (instance == null){…} ,那么就会创建不止一个类的实例了。
Version 2.0
既然Version1.0在多线程环境下无法正常工作,那么最直接的解决办法,用synchronized互斥锁来达到线程安全的运行环境。
|
|
相比于Version1.0,只是在getInstance函数加上synchronized修饰符。但是,我们知道synchronized是互斥锁,就意味着对阻塞线程,很大影响了程序的访问效率。
Version 3.0
为了解决synchronized互斥锁带来的效率上的影响,我们引进了双重检查的机制,代码如下:
|
|
上述代码,看起来很美好,但是存在隐患。
首先,我们要知道instance = new Single(); 这个语句在JVM中,实际执行的原子语句是哪些?
- 给Single分配内存
- 调用Single的构造函数完成初始化,返回类实例
- 将Single对象指向之前分配的完成初始化的内存
但是,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。那么,上述1-2-3的指令执行顺序有可能就变成了1-3-2
我们假设一种情况,线程A和线程B同时访问getInstance()方法。线程A和线程B分别运行在上述代码注释所在地。
假设,线程B的new Single()操作被指令重排了的,执行到了1-3:已经将分配的内存指向了instance对象,但是并未执行初始化操作,所以这个时候instance对象不等于null,但是没有被初始化。然后,线程B让出CPU,线程A开始执行,判断if (instance == null),这时我们可以知道instance是不等于null的,于是线程A就直接返回这个instance对象,但是这个对象是线程B没有初始化的对象!
问题的关键在于:线程B对instance的写操作没有完成,线程A就执行了读操作。
Version 4.0
继续改进我们的懒汉式单例实现代码:针对Version 3.0的双锁检查可能遇到的指令重排带来的线程不安全,Java语言提供了volatile关键词,来帮助我们禁止指令重排。更多关于Java内存模型的内容参考这篇博客。
|
|
以上就是线程安全的懒汉式单例的最终的写法。
饿汉式单例
饿汉式单例:指全局的单例实例在类装载时构建的实现方式
静态内部类实现
|
|
SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个线程安全的单例。
枚举实现
|
|
enum的语法结构虽然和class的语法不一样,但是经过编译器编译之后产生的是一个class文件。该class文件经过反编译可以看到实际上是生成了一个类,该类继承了java.lang.Enum
使用枚举其实和使用静态类内部加载方法原理类似。java.lang.Enum
另外,由于enum是通过继承了Enum类实现的,enum结构不能够作为子类继承其他类,但是可以用来实现接口。此外,enum类也不能够被继承,在反编译中,我们会发现该类是final的。enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。
最后对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,即Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。
从现在来看,枚举已经成为了单例模式的最佳实践。