java并发编程笔记之volatile关键字
Volatile关键字的作用主要有如下两个:
1. 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。
volatile自身的特性
理解volatile特性的一个好方法是把volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
- 可见性:对于一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写。
- 原子性:对任意单个volatile变量的读/写具有原子性(即使是64位的long型和double型变量),但类似于volatile++这种复合操作不具有原子性
volatile的内存语义
- volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
volatile内存语义的实现
JMM在执行程序语句时,出于优化的考虑可能会对执行语句进行重排序,而为了实现volatile内存语义,JMM会对重排序做一定的限制。
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。【这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后】
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。【这个规则确保volatile读之后的操作不会被编译器重排序到volatile写之前】
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
【问题】:为什么要禁止volatile读/写和普通读/写间的重排序?
引用自 https://blog.csdn.net/Unknownfuture/article/details/105023355
volatile int a = 0;
int b = 1;
public void A (){
b = 2; // 1 普通写
a = 1; // 2 volatile写
}
public void B() {
int c = 0;
if (a == 1) // 3 volatile读
c = b; // 4 普通写
}
假如不禁止
- 那么由于代码1和代码2两处没有数据依赖性,所以二者是可以重排序的。
- 我们假设代码2在代码1之前被执行,此时由于a是volatile变量,所以将a = 1, b = 1刷新进入主内存;
- 如果这时候方法A所在的线程cpu时间片用完了,轮到了方法B在另一个线程中执行,由于a是volatile变量所以代码3处执行的时候会将b = 1, a = 1从主内存中读出,此时代码4再执行的话c会变为1,而不是预想的2(按照书写的顺序来看,a=1发生在b=2之后)。
发生这种错误的原因在于:volatile变量写操作与在其之前的代码发生了重排序,使得刷新内存的时机提早了,可能会漏掉我们写在volatile变量赋值操作之前的那些共享变量的修改。
而这例子也对应:
第一个操作为普通读/写,第二个操作为volatile写,也验证了确实是不能重排序,因为是可能会产生影响的。
换成方法B先执行,代码3和4由于happens-before中的猜测执行机制会重排,用同样的思路可以推出volatile读
内存屏障
为了实现volatile的内存语义,编译器在生成字节码时,会在指今序列中插人内存屏障来禁止特定类型的处理器重排序。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
这里比较有意思的是, volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile读的前面插人一个StoreLoad屏障。
从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。
volatile关键字与Synchronized比较
引用 https://blog.csdn.net/xinghui_liu/article/details/124379221
- Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
- Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
- Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
- volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。