Loading

多线程——volatile

多线程——volatile


定义

【在JVM 1.2之前,Java的内存模型实现总是从主存读取变量】

​ 1.(适用于Java所有版本)volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。

​ 2.(适用于Java5及其之后的版本)volatile 的读和写创建了一个happens-before关系,类似于申请和释放一个互斥锁

作用

  1. 保证变量的可见性,但不保证原子性
  2. 禁止指令重排序

使用条件

  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。

(不具备原子性,不可在有原子性需求的场景下使用)

使用建议

​ 在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。

示例1

public class Demo1 {
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"读 flag="+flag);
        // 线程1 读flag的值
        new Thread(() -> {
            while (true) {
                if (flag) {
                    System.out.println("线程1 读 flag=" + flag);
                    System.exit(0);
                }
            }
        }).start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 线程2 写flag的值
        new Thread(() -> {
            while (true) {
                flag = true;
            }
        }).start();
    }
}

结果

  • main读 flag=false
    线程1 读 flag=true

在使用了volatile关键字之后,线程1拿到了主内存中共享变量flag的值,结果符合预期。

示例2

public class Demo2 {
    private static volatile int flag = 0;

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"读 flag="+flag);
        // 线程1 读flag的值
        new Thread(() -> {
            while (true) {
                if (flag == 10) {
                    System.out.println("线程1 读 flag=" + flag);// 注释0 打印语句会引起happens-before
                    System.exit(0);
                }
            }
        }).start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 线程2 写flag的值
        new Thread(() -> {
            while (true) {
                flag = 10; // 注释1 对flag的写操作依赖当前值
//                flag++; // 注释2 对flag的写操作依赖当前值,当取消注释0处的打印语句会陷入死循环
            }
        }).start();
    }
}

flag=10,结果:

  • main读 flag=0
    线程1 读 flag=10

flag++, 同时取消打印语句,结果:

  • main读 flag=0

    之后陷入死循环

示例3

public class Demo3 {
    private static volatile int flag = 0;
    private static volatile int v = 0;
    private static volatile int a = v + flag; // 该变量没有包含在具有其他变量的不变式中
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"读 flag="+flag);
        // 线程1 读a的值
        new Thread(() -> {
            while (true) {
                if(a == 10) {
                    System.exit(0);
                }
            }
        }).start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 线程2 写v的值
        new Thread(() -> {
            while (true) {
                v = 10;
            }
        }).start();
    }
}

结果:

  • main读 flag=0

    之后陷入死循环

示例2和示例3验证了volatile的使用条件,这是表明了volatile不具备原子性的特点。

原理实现

使用volatile后,发现汇编代码里对变量入栈操作时加了lock前缀,lock前缀的作用是禁止指令重排序,具体来说相当于加了一个叫内存屏障的东西,内存屏障会出现在共享变量的读写操作之间,来保证可见性。详细可见参考4

  volatile会在部分读写操作间产生内存屏障导致不能重排序,下图时JMM针对编译器制定的重排序规则表。

image-20201119195507839

下面展示volatile读写操作和普通操作相邻时出现的重排序情况

  第一个操作volatile写a,第二个操作普通读a,可以看出发生了重排序

image-20201119205354260

  第一个操作volatile写a,第二个操作普通写b,可以看出发生了重排序

image-20201119205246427

  前一个普通写c,后一个个volatile读v,可以看出发生了重排序、

image-20201119210537722

  第一个操作普通读b,第二个volatile读v,可以看出发生了重排序

image-20201119211828321

JMM为实现volatile内存语义,提供下面4种读写操作策略

  1. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  2. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  3. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  4. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

image-20201119213749294

image-20201119213929114

  上面4种策略如果都同时采用的话时最保守的情况, 不过实际种内存屏障处理器的内存模型层面可以继续优化。例如X86处理不会对读读,读写,写写操作进行指令重排序。所以,在X86中,JMM不用考虑三种类型的内存屏障,仅需在volatile写后面加一个LoadStore内存屏障即可,不过执行LoadStore开销巨大。

参考

  1. volatile-百度百科
  2. volatile-维基百科
  3. Java中volatile关键字的最全总结
  4. 深度解析volatile—底层实现
  5. 深入理解Java内存模型
posted @ 2020-11-17 21:54  齐玉  阅读(287)  评论(0)    收藏  举报