前言
volatile关键字在Java中多线程编程中作为必不可少的关键字,它的作用和原理你知道多少?在我们线程之间通信有很多种方式,它主要是作用在什么方式中呢?在这种通信方式中它是通过什么方式来实现线程之间的数据安全呢?volatile在CPU执行多个线程中占有什么样的角色?
一篇文章和小伙伴一起探索volatile为什么能对我们的线程安全有帮助。看看它是站在哪位巨人的肩膀上的。
多线程内存模型
在一个进程启动之后系统会为这个系统分配一篇固定的内存空间作为共享内存。进程中每个线程创建之后会获得一块运行内存,执行完成之后还回共享内存。
线程之间的数据是不共享的,必须要通过主内存才能实现内存共享。
线程通信
线程通信的几种方式
-
通过本地文件通信;例如:
日志文件
-
通过网络通信;例如:
数据库
,Redis
,MQ
等等 -
通过本地变量通信;例如:
wait/notify
;
前两种线程通讯方式用的比较普遍,而且也和volatile并无半毛钱关系。重点来看下第三种方式,上面的wait/notify只是本地变量通信的其中一种方式。而我们更加常用的并不是它,我们更常用的是这样的:
public class VolatileTest {
private static boolean stop = false;
public static void main(String[] args) {
//线程1
new Thread(() -> {
while (!stop){
}
}).start();
//线程2
new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
}
stop = true;
}).start();
}
}
该示例中当线程2把stop标识改为true的时候,如果按照正常的思维线程1应该结束循环。但是事实上测试结果如下:线程1处于无休止的循环中。
注意:
这里有个知识点 => 进程中的所有线程(除了守护线程之外)执行完成之后,进程才会结束。
volatile在线程通信中的作用
上例中可以看出,线程2修改了全局变量stop,线程1并没有
拿到修改结果。那我们不妨以volatile
关键字来修饰静态全局变量stop,再来看下执行结果:
private volatile static boolean stop = false;
结果显而易见,在加了volatile关键字之后,在一个线程中被volatile修饰的变量可以被另外用到它的线程发现。这个特性就是我们常说的变量在线程中的可见性
。可见性,原子性和安全性是线程间通信的三大特性保证。
volatile可以保证全局变量在线程中的可见性
以及通过线程栅栏阻止JVM和CPU的指令重排序实现单指令原子性
。
下面我们来看看volatile在线程通信中的可见性是如何实现的。
CPU的MESI协议
CPU 的mesi协议简单来说就是一个线程在工作内存中修改一个全局数据的时候需要经历如下几个步骤:
关于MESI的定义逻辑如上所述,这里有关于MESI的一个比较官方的网站有详细介绍,而且还有上述逻辑的动画参考。网址分享在这里,有兴趣了解的小伙伴可以上来看看。这里就不在赘述。
volatile关键字的底层可见性实现
突然发现上面CPU的MESI协议的内容和我们文章开始的示例一毛一样。那么我就可以明确的告诉大家,实际上我们的volatile就是使用的CPU的MESI协议来实现变量可见性的。不服往下看:
这是没有volatile关键字代码编译后的汇编指令:
这是有volatile关键字代码编译后的汇编指令:
这里的lock
指令在我们的X86体系结构中就是MESI的实现,所以volatile关键字能实现共享变量的线程可见性,同时就保证了指令层面的单指令的原子性
。
volatile关键字禁止指令重排序保证单指令原子性
安全性
是指程序的执行结果与我们预期一致的特性,说简单点就是保证执行结果正确。
指令重排序
指JVM虚拟机和CPU在执行指令之前在不改变原语义的前提下会对用户指令进行一系列的优化的一种骚操作,简单说就是系统会帮你优化代码。
禁止指令重排序是根据内存屏障(Memory Barrier)
来实现的,禁止重排序之后的指令执行顺序和我们程序执行顺序一样的。
内存屏障包含:读读屏障(LoadLoad)
,读写屏障(LoadStore)
,写读屏障(StoreLoad)
和写写屏障(StoreStore)
四种。
在对volatile修饰变量的操作中jvm就会使用上面的四种屏障禁止其他指令与本操作进行重排序,然后内存系统会禁止对lock指令修饰的指令重排序;这就是volatile关键字能保证单指令操作原子性的原理。
注意:
i++ 操作不是单指令操作。
结语
Java中的线程安全是通过CAS锁 + volatile 或者 synchronized 关键字实现。CAS保证操作的原子性,synchronized关键字是在C++中实现,是对象锁。
JDK中的juc包提供了许多上述锁的应用类,比如:ConcurrentHashMap。JDK源码中的很多思想和代码对我们的成长有很大的帮助。建议小伙伴们在上面花点时间。