为什么单例模式的mInstance前面要加volatile
有的老铁看到标题后会有疑问,没遇到过,mInstance要加volatile吗?
还有的人的以为是,synchronized不是可以保证原子性和线程安全吗,怎么还要加volatile?
本次要聊的是单例模式的双重检验加锁实现方式,如下:
public class Single {
private static volatile Single mInstance = null;
private Single() {
}
public static Single getInstance() {
if (mInstance==null) {
synchronized (Single.class) {
if (mInstance==null) {
mInstance = new Single();
}
}
}
return mInstance;
}
}
复制代码
这个volatile该不该加?
聊聊volatile的特性
- 可见性
可见性是指线程对共享变量进行修改的指令对其他线程来说是可见的,反应指令执行的透明度。
对共享变量的修改操作有哪些?读和写
每个线程有自己的本地内存,本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存进行,操作结束后 再同步到堆内存中。从操作开始到结束这段时间,其他线程是不知道的,即操作不可见。当变量被volatile修饰,任何操作都会在内 存中进行,不会产生副本,从而保证了可见性。
- 防止指令重排序
指令重排序是指CPU在处理信息的时候会对一些读数据或者写数据的指令做一个合并进行的优化。
private void operation() {
//第一处
int a = 0;
int b = 1;
int c = 2;
//第二处
int sum = a+b+c;
//第三处
int d = 3;
}
复制代码
如上第三处的写操作会被优化放到第二处前执行。若对第三处使用volatile修饰可以让CPU把第三处操作放在第二处操作之后再执行。
- 无原子性
volatile int e = 0;
private void addforone() {
e++;
}
复制代码
e++操作包含读取e,执行e+1,和将e+1赋值给e。第一个线程读取了值,然后执行+1操作,最后把值存到主存。 单个线程是没问题的,多线程情况下,第二个线程在第一个线程把值存到主存之前读取了值,然后+1,再把值存 到主存,这时就会有问题,预期结果是原值+2,实际结果却是原值+1。
回到单例模式
第一个线程调用getInstance(),开始执行mInstance = new Single(),初始化Single实例和将对象地址写到mInstance并非原子操作,且这两个阶段 执行顺序不确定。由于java编译器的优化导致对象初始化还没完成,mInstance引用值提前写入。假设第一个线程的new Single() 执行到构造方法之前,构造方法还没调用,编译器只是为该对象分配了空间并设置默认值。若此时第二个线程调用getInstance(), 由于mInstance!=null,但是mInstance指向的对象未被赋予正确的值,第二个线程获取到的不是正确的单例对象。
结语
为了避免获取不到正确的单例对象,要用volatile修饰单例对象。