volatile关键字

97 阅读5分钟

JVM中提供的四类内存屏障指令

  • loadload:
    读读,该屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
  • storestore:
    写写,该屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中
  • loadstore:
    读写,该屏障用来禁止处理器把上面的volatile读与下面的普通写重排序
  • storeload:
    写读,该屏障的作用是避免volatile与后面可能有的volatile读/写操作重排序

volatile 特性

  • 保证可见性
  • 禁止指令重排
  • 不保证原子性

volatile如何保证可见性

public class VolatileTest {

    /**
     * 使用volatile,1秒钟后程序会停止
     * 不使用volatile,则不会停止
     */
    static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start");
            while (flag) {

            }
            System.out.println("over");
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            flag = false;
        }, "t2").start();
    }

}
复制代码

Java内存模型中定义的8种工作内存与主内存之间的原子操作

read读取

load加载

use使用

assign赋值

store存储

write写入

lock锁定

unlock解锁

  • read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
  • load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
  • use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
  • assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
  • store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
  • write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量 由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
  • lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
  • unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

volatile如何禁止指令重排

    1. volatile有关禁止指令重排的行为

    • 当第一个操作是 volatile 读时,不论第二个操作是什么,都不能重排序;这个操作保证了volatile读之后的操作不会被重排到volatile读之前
    • 当第二个操作为 volatile 写时,不论第一个操作是什么,都不能重排序;这个操作保证了volatile写之前的操作不会被重排到volatile写之后
    • 当第一个操作为 volatile 写时,第二个操作为 volatile 读时,不能重排序
    1. 四大内存屏障插入情况(具体见下面代码分析)

    • 在每一个 volatile 写操作前面插入一个 storestore 屏障
    • 在每一个 volatile 写操作后面插入一个 storeload 屏障
    • 在每一个 volatile 读操作后面插入一个 loadload 屏障
    • 在每一个 volatile 读操作后面插入一个 loadstore 屏障

volatile为什么不保证原子性?

volatile变量的复合操作(如i++)是不具有原子性的,原因是 i++ 操作从字节码角度来看,是分为三步的

11.png

多线程环境下,数据计算和数据赋值操作可能多次出现,即操作非原子。若数据再加载之后,若主内存中 count变量发生修改之后,由于线程工作内存中的值在此之前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致

对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。

使用内存屏障分析下面代码

public class VolatileTest {

    int i = 0;
    volatile boolean flag = false;

    public void write() {
        i = 1;
        flag = true;
    }

    public void read() {
        if (flag) {
            System.out.println("i=" + i);
        }
    }

}
复制代码

写操作:

  • 在每一个volatile写操作 前面 插入一个 storestore屏障
  • 在每一个volatile写操作 后面 插入一个 storeload屏障
操作说明
i = 1普通写
storestore屏障禁止上面的普通写与下面的volatile写重排序
flag = truevolatile写
storeload屏障禁止上面的volatile写与下面可能有的volatile读/写重排序

读操作:

  • 在每一个volatile读操作 后面 插入一个 loadload屏障
  • 在每一个volatile读操作 后面 插入一个 loadstore屏障
操作说明
if (flag)volatile读
loadload屏障禁止上面的volatile读与下面的普通读重排序
loadstore屏障禁止上面的volatile读与下面的普通写重排序
System.out.普通读

happens-before

  • 顺序执行原则

    • 一个线程中的操作 happends-before 该线程下后面的操作,也就是单线程下代码顺序不管怎么变,结果不变
  • volatile 规则

    • 对于 volatile 修饰的变量的写操作 happends-before 之后的读操作
  • 传递规则

    • 1 happends-before 2,2 happends-before 3,所有 1 happends-before 3
  • start 规则

    • start 启动之前的操作 对于 线程可见
  • join 规则

    • join 启动之前的操作 对于 线程阻塞
  • 监视器规则

    • 锁对象的释放 happends-before 锁对象的加锁

happends-before :表示 前者的操作对后者可见