volatile关键字在java多线程中有着比较重要作用,volatile主要作用是可以保持变量在多线程中是实时可见的,是java中提供的最轻量的同步机制。
可见性
在Java的内存模型中所有的的变量(这里的变量是类全局变量,并不是局部变量,局部变量在方法内并没有线程安全的问题,因为变量随方法调用完成而销毁)都是存放在主内存中的,而每个线程有自己的工作内存,每次线程执行时,会从主内存获取变量的拷贝,对变量的操作都在线程的工作内存中进行,不同线程之间也不能共享工作内存,只能从主内存读取变量的拷贝。具体可以通过下图来表示:
然而对于volatile(使用synchronized/final修饰都具有可见性)来说打破了上述的规则,打破规则的根本原因是在volatile修饰的变量前后会插入内存屏障且在执行volatile域写时加上LOCK指令,LOCK指令的作用就是当线程修改了变量的值,其他线程可以立即知道该变量的改变。当被volatile修饰的变量在写入数据时,会将最新的值写入主内存中,并且这个写入会导致各个cpu的缓存失效,迫使当前线程必须从主内存中读取。然而对于普通变量来说,当一个线程修改了变量,需要先将变量写回主内存,其他线程从主内存读取变量后才对该线程可见。似乎从以上的描述可以推导出只要使用volatile修饰的变量就可以保证该变量在多线程环境下操作是安全的,因为它对于所有线程的工作内存都是可见的也就是说一致的。这么理解确实没错,但是在java中很多运算都不是原子的,所以在java的一些运算中使用volatile并不能保证线程安全问题。让我们来看一个例子:
public class test{
private static volatile t=0;
private static int add(){
return t++;
}
public static void testVolatile(){
for (int i=0;i<20;i++){
Thread thread=new Thread(()-> {
for (int j=0;j<1000;j++) {
add();
}
});
thread.start();
}
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.println(t);
}
public static void main(String[] args){
testVolatile();
}
}
预期这个t值应该是20000,但是会出现t值小于20000的情况,原因大家应该猜到了,问题出在t++上,t++并不是一个原子操作,t++的操作在java中代表先获取t值,再加1,再赋值还t。在获取t值时因为是volatile修饰的,所以可以获取线程最新值,然而在加1的时候就不能保证了,有可能其他线程已经加1了。
那么什么场景使用volatile是最合适的呢?
- 在变量运算不依赖当前值
- 变量不需要与其他状态变量共同参与不变约束
翻译成中文就是对于那些在volatile 不可以用来做getAndOperate的操作,单独的设值和读取才是线程安全的,如果你的代码只有简单的设值和读操作就不要使用lock/synchronized比较重的操作了,直接读就是,因为变量是可见的。最佳实践就是使用volatile修饰的标志位来控制线程的停止,例如:
public class Test implement Runnable {
private volatile boolean start=false;
public void stop() {
start = false;
}
@override
public void run(){
if(!start) {
return;
}
}
}
内存屏障
在说内存屏障前需要提出另一个专有名词–重排序,重排序是编译器或者处理器为了优化程序运行性能而对指令重新编排执行的一种方法。值得一提的是重排序可以保证在单线程的情况下指令重排后和为重排的执行结果是一致的,所以前面强调了是优化性能的一种方法,在单线程下没问题但是在多线程并发的情况下可就不一定了。
内存屏障是一组处理器指令,用以实现对内存操作的顺序限定。内存屏障就是为了防止上述的重排序准备的,在对volatile域进行写入和读取时,编译器会在写入前插入StoreStore内幕屏障,在写入域后插入StoreLoad内存屏障,已保证在对前一次写肯定happen before第二次写,第一次写hanppen before后一次读操作,在读前插入LoadLoad内存屏障。
总结
volatile关键字可以保证变量的可见性原因是LOCK指令和内存屏障,保证了可见性但是在java中一行代码如:new Object()
或者i++
其实内部是多个指令组成,而volatile只是在最后阶段加入LOCK指令把数据刷入主内存,而前面的赋值和寻址等操作可不是线程安全的所以volatile 不可以用来做getAndOperate的操作,单独的设值和读取才是线程安全的。
参考:
1.深入理解 Java 内存模型(四)——volatile
2.为什么volatile不能保证原子性而Atomic可以?
3.聊聊并发(一)——深入分析 Volatile 的实现原理