Java并发基础 - volatile篇
在上一篇文章中,我们介绍了synchronized
关键字的使用和部分原理,下面我们再来看看在并发编程中另一个非常重要的关键字volatile
。为了直观的体会volatile
的作用,下面先看一段代码:
public class VolatileTest {
private static boolean flag=false;
public void setFlag(){
this.flag=true;
System.out.println(Thread.currentThread().getName()+" change flag to true");
}
public void getFlag(){
while(!flag){
}
System.out.println(Thread.currentThread().getName()+" get flag status change to true");
}
public static void main(String[] args) {
VolatileTest test=new VolatileTest();
new Thread(test::getFlag).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(test::setFlag).start();
}
}
例子中使用两个线程来对boolean
类型的flag进行修改和读取。讲道理当执行getFlag
方法的线程检测到flag变为true
时,应该退出循环并打印语句。但是看一下执行结果,会发现只打印了setFlag
方法中的语句,并且程序一直没有执行结束。
下面,我们在flag加上volatile
关键字修饰,再执行一次上面的代码:
private static volatile boolean flag=false;
可以看到,这时getFlag
方法的线程检测到了flag的变化,并正常结束了程序。结合上面的例子,我们发现,当一个线程写数据,另一个线程读数据时,会存在数据不一致性的问题,而volatile
的出现正好解决了这个问题。那么volatile
究竟做了什么工作呢,这个时候就要引入java内存模型(JMM
)来一探究竟了。
如上图中所示,java中运行的线程是不能直接读写主内存的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。主内存是多个线程共享的,单线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。
JMM控制中,又将对数据原子操作分为以下8个类别:
read
(读取):从主内存读取数据load
(载入):将主内存读取的数据写入工作内存use
(使用):从工作内存读取数值来计算assign
(赋值):将计算好的值重新赋值到工作内存中store
(存储):将工作内存数据写入主内存write
(写入):将store
过去的变量值赋值给主内存中的变量lock
(锁定):将主内存变量加锁,标识为线程独占状态unlock
(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
那么,我们之前举的例子就可以用下面的图来表示:
很明显,线程1无法跳出循环的原因是它读取的一直是自己工作内存中的flag,而没有获取到主内存中更新后的值。
为了解决缓存一致性问题,曾经使用过总线加锁的解决方案。具体来说,就是CPU从主内存读取数据到缓存,会在总线上进行数据加锁,这样其他CPU就没法去读写这个数据,直到这个CPU使用完数据释放锁之后其他CPU才能读取该数据。
但是这样一来,由于加锁的粒度太大,会造成阻塞时间过长,严重降低CPU的使用性能。因此在此基础上,行成了我们现在使用的MESI缓存一致性协议:
简单来说,就是多个CPU从主内存读取同一个数据到各自的缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存,其他CPU通过总线嗅探机制可以感知到数据的变化而将自己缓存里的数据失效。
总结一下,就是在读操作时,不做任何事情,把内存中的数据读到缓存中。而在写操作时,发出信号通知其他的CPU将该变量置为无效,其他的CPU要访问这个变量的时候,只能从内存中获取。
给测试类配置启动参数,打印汇编指令到控制台:
-server -Xcomp -XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
-XX:CompileCommand=compileonly,*VolatileTest.setFlag
可以看出,在执行到修改flag的语句时,首先加入lock
这一个前缀指令,实现了对缓存行的锁定。简单来说,就是:
lock
flag=true write回主内存
unlock
只有在执行写write
操作时候才会加锁,相对总线对数据加锁,极大的降低了锁的粒度,只要不是在write
过程中其他线程依然可以读取主内存中的数据,从而提高了CPU性能
除此之外,volatile
还能够实现指令的有序性。保证有序性是因为有时候会出现代码实际执行的顺序并不是我们输入的代码的顺序,那么为什么会出现这种情况呢,这里就有必要引入一下指令重排序:编译器为了优化程序的性能,会重新对字节码指令排序。
指令重排序的基础是,编译器认为运行的结果一定是正常的。在单线程下,指令重排序对程序的帮助一定是正向的,可以很好的优化程序的性能,但是在多线程下,有可能因为指令重排序出现一些问题。volatile
实现有序性保证了以下两点:
volatile
之前的代码不能调整到它的后面volatile
之后的代码不能调整到它的前面
总结
最后,结合上一篇文章,我们总结一下synchronized
与volatile
的特点以及区别:
- 使用上的区别:
volatile
只能修饰变量,synchronized
能修饰方法和语句块 - 对原子性的保证:
synchronized
可以保证原子性,volatile
不能保证原子性 - 对可见性的保证:都可以保证可见性,但实现原理不同。
volatile
对变量加了lock
,synchronized
使用monitorenter
和monitorexit
- 对有序性的保证:
volatile
能保证有序,synchronized
虽然也可以保证有序性,但是代价变大(重量级),并发退化到串行执行 - 除此之外:
synchronized
会引起阻塞,而volatile
则不会引起阻塞
最后
觉得对您有所帮助,小伙伴们可以点个赞啊,非常感谢~
公众号『码农参上』,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。欢迎来加Hydra好友 (vx: DrHydra9),围观朋友圈,做个点赞之交啊。
- 点赞
- 收藏
- 关注作者
评论(0)