单例模式
定义
一个类只能构建一个对象的设计模式。
实现
懒汉模式
1 | public class LazySingleton { |
只能通过一个私有的构造方法去创建对象,而创建对象的前提是此对象不存在。
一开始 instance
为空,当需要时才会创建对象,称之为:懒汉模式
那么如果一开始就创建 instance
,不在进行判空操作,我们称之为:饿汉模式
饿汉模式
1 | public class HungrySingleton { |
懒汉模式升级版 ——双重加锁
上面的懒汉模式存在线程安全的问题:
- 当
instance == null
的情况下,假设目前有两个线程A、B。两个线程同时调用getInstance
方法,由于instance == null
满足,两个线程同时通过了条件判断,执行new
创建对象。显然这样就会使得本来只想有一个实例的对象出现了多次。
基于此,我们可以更新代码为:
1 | public class LazySingletonCheckTwice { |
使用同步锁的原因:
- 防止
new
多次,在new
操作前加上同步锁,锁住整个类
第二次判断的原因:
- 两个线程会存在同时通过第一重检测的情况,当第一个线程创建对象后,若不进行再一次判空检测,还会再创建一个对象。
不在第一次检测加锁:
- 使用同步锁比两次
if
更加消耗性能,由于单例的缘故,绝大多数情况下访问方法时已经有了单例对象,再第一重加上同步锁反而会带来性能消耗,远不如第一判断获取对象要快。
懒汉模式 ——线程安全终极版
上述的操作过程中,我们实现了相对安全的单例模式的代码。这是并没有做到绝对的线程安全。
真正的原因涉及到 JVM 的 指令重排:
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
————————————————
版权声明:本文为CSDN博主「bladestone」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/blueheart20/article/details/52117761
比如,创建对象:instance = new LazySingletonCheckTwice()
会被编译为:
memory = allocate()
分配内存空间ctorInstance(memory)
初始化对象instance = memory
设置 instance 指向分配的内存地址
然而这些指令可能会经过 JVM 和 CPU 的优化后,重排为:1、3、2 这样的顺序。
我们还是假设有两个线程A、B,当 A 线程创建对象时执行了 1、3,instance 已经不在指向 null
,此刻若线程 B 获得了CPU,将不会通过第一重检测,直接返回一个未完全初始化的对象。
为了防止指令重排所带来的问题,我们可以使用 volatile
关键字:
如何防止指令重排
volatile 关键字可以保证变量的可见性,因为对 volatile 的操作都在Main Memory中,而Main Memory 是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。
volatile 还有一个作用就是局部阻止重排序的发生,对volatile变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题。
在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量干脆都是读写内存。可见性
这里提到的可见性是指前一条程序指令的执行结果,可以被后一条指令读到或者看到,称之为可见性。反之为不可见性。这里主要描述的是在多线程环境下,指令语句之间对于结果信息的读取即时性。
————————————————
版权声明:本文为CSDN博主「bladestone」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/blueheart20/article/details/52117761
静态内部类实现单例模式
1 | public class StaticInnerClassSingleton { |
外部无法访问静态内部类,只有通过 getInstance()
来获取实例,再调用方法的时候,静态内部类才会加载(利用 classloader
的加载机制实现懒加载),同时这种方式也保证了线程安全。
上面的Bug
上面的方法说到底还是使用私有构造器去创建对象,那么 Java 中反射可以打破上面单例的约束,只要设置构造器为可访问,便可以任意调用构造器去创建足够多的对象。
那么如何防止这个逆天Bug呢:
使用枚举
JVM 会阻止反射获取枚举类的私有构造方法
在构造方法中可以这样操作:当已有实例时,抛出异常;没有则创建对象。
1 | private StaticInnerClassSingleton() { |
这里还是用了同步锁,防止两个操作反射的线程在通过 if
判断后,同时通过构造器去创建对象。
这样,在标记变量没有 setter
方法且为私有基本类型的变量的情况下,就算使用反射也无法通过强行更改标记变量的值来创建第二个对象。