volatile是JVM虚拟机提供的最轻量级的同步机制,如果能恰当的使用volatile的话,它比synchronized的执行成本更低,因为它不会引起上下文的切换和调度。
Java语言规范第三版中对volatile的定义如下:Java语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获取这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
上面两段话说明了Java中的volatile是什么以及为什么会出现volatile,实际上,volatile在JVM虚拟机运行时具有两个语义:
1. 可见性。
2. 相对有序性。
以下内容参考“Java并发编程的艺术”
以X86处理器下通过工具获取到的JIT编译期生成的汇编指令来查看对volatile变量进行写操作,会有如下汇编代码:
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多出第二行lock汇编代码,通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发两件事情:
a. 将当前处理器缓存行的数据写回的到系统内存。
b. 这个写回内存的操作会使在其他CPU里缓存了该地址的数据失效。(这点通过缓存一致性协议实现,具体做法:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存把数据读取到处理器缓存中,不直接从内存中修改,是为了提高处理速度,内存的频率远小于CPU的频率,根据木桶效应,所以增加了L1,L2缓存,以提高运算性能)。
可见性释义:
volatile修饰的变量对所有的线程是立即可见的,对volatile的所有写操作都能立即反映到其他线程中。换句话说,volatile变量在各个线程中是一致的, 但这并不能推出基于volatile变量的运算在并发下是安全的。可以推出基于volatile变量的原子操作在并发下是线程安全的。实际上也是如此,引用(周志明老师的“深入理解Java虚拟机-JVM高级特性与最佳实践”)中的一段代码:
/**
* {@link volatile} 测试
* <p>
* 验证volatile是不是线程安全的?参考周志明老师的深入理解JVM高级特性
*/
public class VolatileTest01 {
public static volatile int race = 0;
public static final int THREADS_COUNT = 20;
public static void increase() {
/**
* 产生线程不安全的根本原因:race++不是原子操作,反编译后的字节码如下
*
* public static void increase();
* descriptor: ()V
* flags: ACC_PUBLIC, ACC_STATIC
* Code:
* stack=2, locals=0, args_size=0
* 0: getstatic #2 // Field race:I
* 3: iconst_1
* 4: iadd
* 5: putstatic #2 // Field race:I
* 8: return
* LineNumberTable:
* line 15: 0
* line 16: 8
*/
race++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
});
threads[i].start();
}
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(race);
}
}
这段代码运行后,结果显然不是200000。原因是:虽然volatile保证了race在线程之间的可见性(读取race变量时确实是最新的race值),可是线程读取race之后,再对race进行运算,最后对race进行赋值,整个操作是非原子性的,这之间的时间,足以让别的线程修改了刚才此线程读到的最新的race值,因此才产生了这个bug。
如果将race的读取和运算整体改为原子操作,每次运行结果均为200000,代码如下:
public class VolatileTest03 {
private static volatile AtomicInteger race = new AtomicInteger(0);
public static final int THREADS_COUNT = 20;
public static void increase(){
race.incrementAndGet();
}
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
});
threads[i].start();
}
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(race);
}
}
相对有序性释义:
就是因为(a.将当前处理器缓存行的数据写回的到系统内存),将结果写回内存时,意味着这条指令之前的指令已经得出结果,这就形成了类似于“内存屏障”的功能,导致volatile前后的代码无法指令重排序。为什么说相对有序呢??volatile之前和之后的指令还是可以各自发生重排序的,只是重排序无法穿透volatile而已。
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
-
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
-
关注公众号 『逆行的碎石机』,不定期分享原创知识。
-
同时可以期待后续文章ing🚀