这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
前言
并发编程带来的问题原子性,可见性和有序性问题。前面提到内存模型JMM的概念,JVM有一系列的措施保证多线程的安全问题,比如as-if-serial, happens-before, 还有一些同步的锁机制等等。volatile是Java虚拟机提供的轻量级的同步机制。
volatile 关键字有两个作用
- 可见性,是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 有序性,禁止指令重排
1. volatile 可见性
volatile 内存可见性,JDK很多并发编程源码中都用到了这个特性,比如说AQS中state变量。多线程竞争锁的时候依据这个state标识去判断。
public class VolatileSample {
volatile boolean state = false;
public void save(){
this.state = true;
System.out.println("线程:"+ Thread.currentThread().getName() +":修改共享变量state");
}
public void load(){
while (!state){
}
System.out.println("线程:"+ Thread.currentThread().getName() + "当前线程感知state的状态的改变");
}
public static void main(String[] args){
VolatileSample sample = new VolatileSample();
Thread threadA = new Thread(()->{
sample.save();
},"threadA");
Thread threadB = new Thread(()->{
sample.load(); },"threadB");
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace(); }
threadA.start();
}
}
2. volatile禁止重排优化
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序 执行的现象.
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
一个非常经典的禁止指令重排优化的案例DCL
在多线程环境下面可能会出现问题,原因在于某个线程执行到第一次检测,读取到instance不为null的时候,instance的引用对象可能还没有初始化完成。
因为 instance = new DclDemo(); 可以分为3步
- memory = allocate() 分配对象的内存空间内
- instance(memory); 初始化对象
- instance = memory; 设置instance指向刚分配的内存地址 因为不揍2和3可能会重排序,因为2,3 之间没有依赖关系
- memory = allocate() 分配对象的内存空间内
- instance = memory; 设置instance指向刚分配的内存地址, 此时instance != null ,但是对象没有初始化
- instance(memory); 初始化对象
当然这也是一些极端的情况,发生指令重排了,另外一个线程正好又挂掉了,为了保证这种情况的线程安全,我们可以使用volatile禁止instance变量被执行指令重排优化即可。
private volatile static DclDemo instance;
3. volatile内存语义的实现
前面提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。
是JMM针对编译器制定的volatile重排序规则表。
举例来说,第三行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
参考
《Java并发编程艺术》