单例模式引起的一些思考
单例模式通常有饿汉式和懒汉式,懒汉式
饿汉式
无线程安全性问题
1 | public class SingleHungryStyle { |
懒汉式
单线程下
demo1
1 | public class SingleLazyStyle { |
上述代码在多线程的情况下会出现多个实例,所以需要进行一个加锁判断。
多线程下
demo2
1 | public class SingleLazy1Style { |
上述代码在多线程的情况下运行多次偶尔会出现一个问题,就是CPU的重排序会导致instance
还未完全初始化就被使用了。
例如:
此时线程二
就有可能报错,因为JVM在进行一个类的初始化的时候是分为三步的。
Java SE 8
的JVM规范里面对一个类的加载进行了详细的描述:Java SE 8的JVM规范。
具体来说就是分为三步:
- Loading
- Linking
- Initializing
Creation and Loading
在这一步,JVM需要判断需要初始化的类是数组
还是一个普通类
,如果是一个普通类的话,就再进行判断是需要使用bootstrap class loader
来进行加载还是说用user-defined class loader
进行加载。
Linking
在这一步主要是验证
和准备
- Verification
- Preparation
- Resolution
而将未初始化的引用绑定到实例上就是Resolution
。具体可以参考: Java SE 8的JVM规范 5.4.3
Initializing
初始化,即将字段进行一个默认值初始化。
但是这里因为Linking
和Initializing
之间并无任何的关联性,所以可能会导致先进行了一个初始化,但是并未将该引用绑定到堆的一个实例上,而此时由轮到另一个线程执行。所以就会导致另一个线程获取的是空对象。
问题
但是在这个初始化的过程中,Linking
和Initializing
之间由于互相不依赖,所以CPU
可能会先进行初始化,但是并未进行关联
,即将引用关联到JVM里面的一个实例。而直接返回了。此时由于happens-before原则
并不能跨线程,所以会出现两种情况:
- 如果线程一在线程二之前使用了
instance
,此时线程二使用instance
不会出现任何问题 - 如果线程一在初始化完毕之后释放了锁资源,然后线程二执行,因为线程二判断
instance
已经被初始化了(但此时实际上并未Linking),所以在使用的时候会报错。
但是这个时候,由于CPU的重排序
,导致线程二
获取的instance
可能出现空指针异常;
所以一般为了避免这种情况,会加一个volatile
关键字来禁止内存重排序。
demo3
1 | public class SingleLazy2Style { |
volatile
在这里之所以使用volatile
的特性之一:防止内存进行重排序(包含写屏障和读屏障)
happens-before原则
1 | Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second. |
happens-before原则
要求在一个线程内,重排序后执行的结果与未重排序之前的执行结果必须一致。所以在demo1
中单线程里面,是不会出现任何问题的,因为即使发生重排序,最后在使用instance
的时候,instance也一定会完成初始化,否则就是编译器bug了。
但是在多线程的情况下,happens-before
原则无法生效,所以就会导致其他线程在获取实例的时候会出现异常。
所以在多线程的情况下需要使用volatile
关键字进行修饰,主要是因为需要确保首个初始化的线程必须完成整个类的初始化的操作。