前言
volatile是什么?
volatile解决什么问题?
volatile的实现原理?
带着问题出发,围绕并展开。
volatile是什么?
volatile是java提供的轻量级同步机制,volatile关键字有2个如下作用
1、保证volatile修饰的共享变量对所有线程是可见的,也就是相当于当一个线程修改了一个被volatile修饰的共享变量时,新值可以被其他线程立即感知到。
2、可以禁止指令重排、处理器优化问题。
1、volatile的可见性
可见性案例
线程A改变initFlag属性之后,线程B马上感知到。
public class VolatileVisibilitySample {
volatile boolean initFlag = false;
public void save(){
this.initFlag = true;
String threadname = Thread.currentThread().getName();
System.out.println("线程:"+threadname+":修改共享变量initFlag");
}
public void load(){
String threadname = Thread.currentThread().getName();
while (!initFlag){
//线程在此处空跑,等待initFlag状态改变
}
System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
}
public static void main(String[] args){
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{
sample.save();
},"threadA");
Thread threadB = new Thread(()->{
sample.load();
},"threadB");
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
2、volatile无法保证原子性,为什么?
一个简单的多线程例子(无法保证原子)
public class VolatileVisibility {
public static volatile int i =0;
public static void increase(){
i++;
}
}
在并发的情况下,i变量的任何改变都会立刻反应到其他线程,但是在多线程的情况下,都调用increase()方法,就会出现线程安全问题。
因为i++操作本身并不是原子操作。
i++操作的实际顺序是分为读(通过指针或者常量池获取到值)、改(二个值的计算)、写(最后进行赋值)
i++可以分为如下三步
int x = i;//多线程的情况,线程1在这里读到了值,然后让出cpu资源,线程2拿到资源也读到了x=i的情况就会出现原子性问题
x = i+1;
i = x;
我们知道,因为我们所写的代码,最终是化为一条条指令在CPU上执行,而CPU执行的时间片,又是看快速轮动交替执行的,这样就有可能。
假设造成A线程刚读到值、或者刚改变了新值,还没来得及进行最后的赋值操作,时间片被分配到了B线程,B线程这时候拿到的公共区域的数据,还是A线程没有修改之前的值,然后进行i++,然后执行完了I++操作后,A线程又拿到了线程,执行完了最后的赋值操作,这样就相当于2个线程都是在i=1的基础上进行了+1,最终结果还是2。这就造成了线程不安全。
总结volatile不能解决原子性问题,可以通过syn、lock锁来保证。
3、volatile禁止重排优化问题
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
指令重排和处理器优化,一般是建立在单线程不会影响原本结果的情况下,进行指令重排。
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
禁止重排优化的例子
public class DoubleCheckLock {
private volatile static DoubleCheckLock instance;
private DoubleCheckLock(){}
public static DoubleCheckLock getInstance(){
//第一次检测
if (instance==null){
//同步
synchronized (DoubleCheckLock.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)
memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤1和步骤2间可能会重排序,如下:
memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。
//禁止指令重排优化
private volatile static DoubleCheckLock instance;