Java并发编程-volatile

203 阅读3分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

前言

并发编程带来的问题原子性,可见性和有序性问题。前面提到内存模型JMM的概念,JVM有一系列的措施保证多线程的安全问题,比如as-if-serial, happens-before, 还有一些同步的锁机制等等。volatile是Java虚拟机提供的轻量级的同步机制

volatile 关键字有两个作用

  1. 可见性,是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
  2. 有序性,禁止指令重排

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

image.png

在多线程环境下面可能会出现问题,原因在于某个线程执行到第一次检测,读取到instance不为null的时候,instance的引用对象可能还没有初始化完成。

因为 instance = new DclDemo(); 可以分为3步

  1. memory = allocate() 分配对象的内存空间内
  2. instance(memory); 初始化对象
  3. instance = memory; 设置instance指向刚分配的内存地址 因为不揍2和3可能会重排序,因为2,3 之间没有依赖关系
  4. memory = allocate() 分配对象的内存空间内
  5. instance = memory; 设置instance指向刚分配的内存地址, 此时instance != null ,但是对象没有初始化
  6. instance(memory); 初始化对象

当然这也是一些极端的情况,发生指令重排了,另外一个线程正好又挂掉了,为了保证这种情况的线程安全,我们可以使用volatile禁止instance变量被执行指令重排优化即可。

private volatile static DclDemo instance;

3. volatile内存语义的实现

前面提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。

是JMM针对编译器制定的volatile重排序规则表。

image.png

举例来说,第三行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

参考

《Java并发编程艺术》