大家先仔细看下这张图。
volatile、CAS作为基础类被很多组件(锁、线程池、阻塞队列、并发容器)所依赖和使用。
可见它在整个并发编程领域的地位!
今天我们一起来聊一聊这个基础类的相关特性。
能保证可见性,在“一写多读”场景下比synchronized性能更好,因为不需要线程切换与调度(JMM的线程和OS的线程为一对一匹配:线程模型)。
多写场景下,无法保障”原子性“ 的原因分析:
先看这段代码:
public static volatile int race = 0;
public static void increase() {
race++;
}编译后的字节码:
public static void increase();
Code:0:
getstatic #2 // Field race:I3:
iconst_14:
iadd5:
putstatic #2 // Field race:I8:
returnvolatile方式的i++,总共是四个步骤:
i++实际为load、Increment、store、Memory Barriers 四个操作。
我们从字节码层面来分析可能会导致保障不了原子性原因:
当getstatic指令把race的值取到操作栈顶的时候,volatile关键字保障了race在此时是正确的,但是在执行iconst_1、iadd的时候,其他线程很有可能已经把race值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存。
所以这种情况下还是需要加锁(synchronized或JUC中的原子类)来保障原子性!
CopyOnWriteArrayList
作为一写多读的应用,并发包的copyonArrayList有典型的应用。
它在修改数据的时候,会对整个集合的数据复制出来,对写加锁,修改完成后,再用setArray把array指向新的集合。
使用volatile可以尽快让其他线程感知到array的变化,不进行指令重排。
先看一段volatile实例变量的代码:
public class MySingleton {
private static volatile MySingleton instance = null;
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
public static void main(String[] args) {
MySingleton.getInstance();
}
}
汇编代码
0x00000000027df0d5: lock add dword ptr [rsp],0h ;*putstatic instance; - com.dunzung.demo.MySingleton::getInstance@13 (line 9)计算机处理过程如下:
如果一旦对声明了volatile的变量进行了写操作,那么JVM就会像处理器发送一条Lock前缀的指令,将这个变量所在的缓存行数据回写到主内存。
其他处理器通过“嗅探”机制,发现自己缓存行变量内容(处理器)对应的主存地址发生了变化,就会将当前处理器的缓存行设置为无效状态。
那么就会重新从主存中读取变量值,这其实就保证了可见性!
写的内存语义
当写一个volatile变量时,JMM会将此变量刷新到主内存。
读的内存语义
当读一个volatile变量时,JMM会把线程对应的本地内存置为无效状态,下次强制从主存读取变量值。
为了实现这两者内存语义,JMM制定了“重排序规则表”
为了实现这两种内存语义,JMM限制了编译器和处理器的重排序。
底层是通过“内存屏障”来禁止特定类型的指令重排序。
JMM通过happens-before机制向程序员提供有序性保证,防止指令重排。
两个操作的有序性可以是一个线程内发生或两个不同线程发生。只要符合Happens-before规则。
Happen-before规则如下:
本文完!
如果这篇文章你看了对你有帮助或启发,麻烦关注、点赞一下作者。你的肯定是作者创作源源不断的动力。谢谢!
同时推荐大家关注作者公众号:陶朱公Boy
里面不仅汇集了硬核的干货技术、还汇集了像左耳朵耗子、张朝阳总结的高效学习方法论、职场升迁窍门、软技能。希望能辅助你达到你想梦想之地!
| 留言与评论(共有 0 条评论) “” |