volatile关键字的作用以及底层原理
前言
在java并发编程中,volatile关键字可以保证数据的可见性和防止JVM指令的重排序,我们接下来深入讲讲这些问题是什么以及volatile关键字是如何解决这些问题的
java的内存模型结构
如下图所示,在当今的java内存结构中,多线程访问的共享变量存储在主内存中,每一个线程拥有它们自己的本地内存,主内存是它们共享的内存空间。当线程读取共享变量之后会将共享变量的副本放入它们的本地内存中,当线程下次读取时会直接从本地内存中读取,当修改共享变量时,会先修改本地内存中的共享变量的副本,而后再将共享变量的最新值刷新到主内存中。
数据的不一致和指令的重排序
数据的不一致问题:从上图的java内存模型中,可能会发生这样一个问题:
如果一个线程要修改共享变量,需要两个步骤:
A1:首先要修改本地内存的共享变量,
A2:然后将共享变量的最新值同步到主内存中,
此时另一个线程想要读取这个共享变量,直接从该线程的本地内存读取,无法读取共享变量的最新值,造成了数据的不一致。
指令的重排序:编译器和处理器为了优化程序的性能,可能会对JVM生成的指令进行重排序,对于单线程来说,为了遵守as-if-serial语义,编译器和处理器不会对具有数据依赖关系的操作进行重排序,但是对于多线程来说,重排序可能会破坏多线程程序的语义。
内存屏障
volatile使用内存屏障来保证数据的可见性和防止指令重排序。内存屏障其实是一种指令JVM,在java编译器在生成JVM指令时插入特定的内存屏障指令。
内存屏障粗分为:
Load Barrier:在指令之前插入屏障,使工作内存或cpu高速缓存中的缓存数据失效,返回主内存中读取最新的数据
Store Barrier:在指令之后插入屏障,强制将缓存区中的数据更新到主内存中
细分为:
LoadLoad Barrier:实例Load1;LoadLoad;Load2,确保Load1的读操作先于Load2以及后续操作执行
StoreStore Barrier:实例Store1;StoreStore;Store2,在Store2以及后续操作执行之前,确保Store1的写操作修改的数据已经刷新到主内存中
LoadStore Barrier:实例Load1;LoadStore;Store2,确保Load1操作先于Store2以及后续操作执行
StoreLoad Barrier:实例Store1;StoreLoad;Load2,在Load2以及后续操作执行之前,确保Store1的写操作修改的数据已经刷新到主内存中
volatile读写插入的内存屏障
在volatile读之后加入了LoadLoad Barrier和LoadStore Barrier,防止volatile读操作和后续的volatile读和volatile写错做重排序
在volatile写前加入了StoreStore Barrier:防止volatile写操作和之前的volatile写操作和其他操作重排序
在volatile写之后加入了StoreLoad Barrier:确保在后续的读操作之前,volatile写操作已经把修改的数据同步到主内存中
volatile不保证数据的原子性
比如i++操作,volatile不保证原子性,因为i++是一个复合操作,包含三部:
获取旧值、更新值、返回新值,如果第二个线程在第一个线程获取旧值和读取新值之间读取i的值,就造成了线程安全问题
volatile的内存语义
volatile写:当写一个volatile变量时,JMM会立即将当前线程本地内存修改的共享变量刷新到主内存中
volatile读:当读一个volatile变量时,JMM会将当前线程的本地内存设置为无效,直接从主内存中读取共享变量