① 🧵volatile的作用
在并发编程中Synchronized
与volatile
都扮演着重要的角色,volatile
是轻量级的Synchronized
,它在并发编程中保证了共享变量的可见性。
可见性的意思是:当一个线程修改了一个共享变量,另外一个线程可以立即读到这个修改的值。
线程对变量的所有操作都必须在线程自己的工作内存中完成,而不能直接读取主存中的变量,这是JMM的规定。所以每个线程都会有自己的工作内存,工作内存中存放了共享变量的副本。而正是因为这样,才造成了可见性的问题。
如果volatile
使用恰当的话,它比Synchronized
的使用成本和执行成本更低,因为它不会引入线程上下文切换的额外调度。
1.1 volatile的特性
特性 | 简介 |
---|---|
可见性 | 对一个volatile变量的读,总是能看到对这个volatile变量最后的写入 |
原子性 | 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。 |
理解volatile
特性的一个办法是把对volatile
变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面是具体的示例:
class VolatileFeaturesExamples{
volatile long vl = 0L; //使用volatile声明64位的long型变量
public void set(long l){
vl = l; // 单个volatile 变量的写
}
public void getAndIncrement(){
vl++; // 复合volatile变量的读/写
}
public long get(){
return vl; // 单个volatile变量的读
}
}
假设有多个线程分别调用上面的方法,这个程序在语义上和下面的程序相似
class VolatileFeaturesExamples{
long vl = 0L;
public Synchronized void set(long l){
vl = l; // 对单个变量的写用同一个锁同步
}
public void getAndIncrement(){ // 普通方法调用
long temp =get(); // 调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public Synchronized long get(){
return vl; //对单个变量的读用同一个锁同步
}
}
因此:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。
1.3 volatile写读的内存语义
写的内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
读的内存语义:
- 当读一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量置为无效。线程接下来会从竹内中读取共享变量。
② 🔐volatile内存语义的实现原理
JMM(JAVA MEMORY MODEL)针对volatile
分别对编译器重排序与处理器重排序进行了限制。下表是JMM针对编译器指定的重排序规则。
是否能重排序 | 第二个操作 | ||
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
举个例子:第三行最后一格的意思是:在程序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile
写,则编译器不能重排序这两个操作。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- 在每个volatile写操作前面插入一个StoreStore屏障。
- 在每个volatile写操作后面插入一个StoreLoad屏障。
- 在每个volatile读操作前面插入一个LoadLoad屏障。
- 在每个volatile读操作后面插入一个LoadStore屏障
如何保证可见性
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主内存中。
public void actor2(I_Result r) {
num = 2;
ready = true; // volatile 赋值带写屏,保证前面的读写操作不会被
// 写屏障
}
读屏障保证在该屏障之后的,对共享变量的读取,加载的都是主内存中的最新值。
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
如何保证有序性
写屏障会确保指令重排序时,volatile写之前的读写操作不会跑到后面。
读屏障会确保指令重排序时,volatile读之后的任何操作不会跑到前面。
👨🚀总结
由于volatile
仅仅保证单个变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁更强大;在可伸缩执行上,volatile
更有优势。