上一篇学习了 synchronized 的关键字, synchronized 是阻塞式同步, 在线程竞争激烈的情况下会升级为重量级锁, 而 volatile 是一个轻量级的同步机制.
前面学习了 Java 的内存模型, 知道各个线程会将共享变量从主内存中拷贝到工作内存, 然后执行引擎会基于工作内存中的数据进行操作处理. 一个 CPU 中的线程读取主存数据到 CPU 缓存, 然后对共享对象做了更改, 但 CPU 缓存中的更改后的对象还没有 flush 到主存, 此时线程对共享对象的更改对其它 CPU 中的线程是不可见的.
而 volatile 修饰的变量给 java 虚拟机特殊的约定, 线程对 volatile 变量的修改会立刻被其他线程所感知, 即不会出现数据脏读的现象, 从而保证数据的 "可见性".
我们可以先简单的理解: 被 volatile 修饰的变量能够保证每个线程能够获取该变量的最新值, 从而避免出现数据脏读的现象.
一, 三个特性
在分析 volatile 之前, 我们先看下多线程的三个特性: 原子性, 有序性和可见性.
1.1 原子性
原子性是指一个操作是不可中断的, 要么全部执行成功要么全部执行失败. 即多个线程一起执行的时候, 一个操作一旦开始, 就不会被其他线程所干扰.
看下面几行代码:
- int a = 10; // 语句 1
- a++; // 语句 2
- int b=a; // 语句 3
- a = a+1; // 语句 4
上面的 4 行代码中, 只有语句 1 才是原子操作.
语句 1 直接将数值 10 赋值给 a, 也就是说线程执行这个语句的会直接将数值 10 写入到工作内存中.
语句 2 实际上包含了三个操作: 1. 读取变量 a 的值; 2: 对 a 进行加一的操作; 3. 将计算后的值再赋值给变量 a.
语句 3 包含两个操作: 1: 读取 a 的值; 2: 再将 a 的值写入工作内存.
语句 4 与语句 2 类似, 也是三个操作.
从这里可以看出, 只有简单的读取, 赋值 (而且必须是将数字赋值给某个变量, 变量之间的相互赋值不是原子操作) 才是原子操作.
1.2 有序性
有序性是指程序执行的顺序按照代码的先后顺序执行.
Java 内存模型具备一些先天的 "有序性", 即不需要通过任何手段就能够得到保证的有序性, 这个通常也称为 happens-before 原则. 如果两个操作的执行次序无法从 happens-before 原则推导出来, 那么它们就不能保证它们的有序性, 虚拟机可以随意地对它们进行重排序.
前面线程安全篇中学习过 happens-before 原则, 可以去前篇看看.
1.3 可见性
可见性是指当一个线程修改了共享变量后, 其他线程能够立即得知这个修改.
而普通的共享变量不能保证可见性, 因为普通共享变量被修改之后, 什么时候被写入主存是不确定的, 当其他线程去读取时, 此时内存中可能还是原来的旧值, 因此无法保证可见性.
synchronized 能够保证任一时刻只有一个线程执行该代码块, 并且在释放锁之前会将对变量的修改刷新到主存当中, 那么自然就不存在原子性和可见性问题了, 线程的有序性当然也可以保证.
下面我们来看看 volatile 关键字.
二, volatile 的使用
一旦一个共享变量 (类的成员变量, 类的静态成员变量) 被 volatile 修饰之后, 那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性, 即一个线程修改了某个变量的值, 这新值对其他线程来说是立即可见的.
禁止进行指令重排序.
2.1 可见性
先看下面的代码:
- public class VolatileTest {
- private static boolean isOver = false;
- private static int a = 1;
- public static void main(String[] args) {
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- while (!isOver) {
- a++;
- }
- }
- });
- thread.start();
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- isOver = true;
- }
- }
这里的代码会出现死循环, 原因在于虽然在主线程中改变了 isOver 的值, 但是这个值的改变对于我们新开线程中并不可见, 在线程的本地内存未被修改, 所以就会出现死循环.
如果我们用 volatile 关键字来修饰变量, 则不会出现此情形
private static volatile boolean isOver = false;
这说明 volatile 关键字实现了可见性.
2.2 有序性
再看下面代码:
- public class Singleton {
- private volatile static Singleton instance;
- private Singleton() {
- }
- public Singleton getInstance() {
- if (instance == null) {// 步骤 1
- synchronized (Singleton.class) {// 步骤 2
- if (instance == null) {// 步骤 3
- instance = new Singleton();// 步骤 4
- }
- }
- }
- return instance;
- }
- }
这个是大家很熟悉的单例模式 double check, 在这里看到使用了 volatile 字修饰, 如果不使用的话, 这里可能会出现重排序的情况.
因为 instance = new Singleton()这条语句实际上包含了三个操作:
1. 分配对象的内存空间;
2. 初始化对象;
3. 设置 instance 指向刚分配的内存地址. 步骤 2 和步骤 3 可能会被重排序, 流程变为 1->3->2
如果 2 和 3 进行了重排序的话, 线程 B 进行判断 if(instance==null)时就会为 true, 而实际上这个 instance 并没有初始化成功, 将会读取到一个没有初始化完成的对象.
用 volatile 修饰的话就可以禁止 2 和 3 操作重排序, 从而避免这种情况. volatile 包含禁止指令重排序的语义, 其具有有序性.
2.3 原子性
看下面代码:
- public class VolatileExample {
- private static volatile int counter = 0;
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < 10000; i++)
- counter++;
- }
- });
- thread.start();
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(counter);
- }
- }
启 10 个线程, 每个线程都自加 10000 次, 如果不出现线程安全的问题最终的结果应该就是: 10*10000 = 100000; 可是运行多次都是小于 100000 的结果, 问题在于 volatile 并不能保证原子性, counter++ 这并不是一个原子操作, 包含了三个步骤: 1. 读取变量 counter 的值; 2. 对 counter 加一; 3. 将新值赋值给变量 counter. 如果线程 A 读取 counter 到工作内存后, 其他线程对这个值已经做了自增操作后, 那么线程 A 的这个值自然而然就是一个过期的值, 因此, 总结果必然会是小于 100000 的.
如果让 volatile 保证原子性, 必须符合以下两条规则:
运算结果并不依赖于变量的当前值, 或者能够确保只有一个线程修改变量的值;
变量不需要与其他的状态变量共同参与不变约束
三, 实现原理
上面看到了 volatile 的使用, volatile 能够保证可见性和有序性, 那它的实现原理是什么呢?
在生成汇编代码时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令, Lock 前缀的指令在多核处理器下会引发了两件事情:
将当前处理器缓存行的数据写回到系统内存.
这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效.
为了提高处理速度, 处理器不直接和内存进行通信, 而是先将系统内存的数据读到内部缓存 (L1,L2 或其他) 后再进行操作, 但操作完不知道何时会写到内存. 如果对声明了 volatile 的变量进行写操作, JVM 就会向处理器发送一条 Lock 前缀的指令, 将这个变量所在缓存行的数据写回到系统内存. 但是, 就算写回到内存, 如果其他处理器缓存的值还是旧的, 再执行计算操作就会有问题. 所以, 在多处理器下, 为了保证各个处理器的缓存是一致的, 就会实现缓存一致性协议, 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了, 当处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器的缓存行设置成无效状态, 当处理器对这个数据进行修改操作的时候, 会重新从系统内存中把数据读到处理器缓存里. volatile 的实现原则:
Lock 前缀的指令会引起处理器缓存写回内存;
一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
当处理器发现本地缓存失效后, 就会从内存中重读该变量数据, 即可以获取当前最新值.
3.1 内存语义
理解了 volatile 关键字的大体实现原理, 那对内 volatile 的内存语义也相对好理解, 看下面的代码:
- public class VolatileExample2 {
- private int a = 0;
- private boolean flag = false;
- public void writer() {
- a = 1;
- flag = true;
- }
- public void reader() {
- if (flag) {
- int i = a;
- }
- }
- }
假设线程 A 先执行 writer 方法, 线程 B 随后执行 reader 方法, 初始时线程的本地内存中 flag 和 a 都是初始状态, 下图是线程 A 执行 volatile 写后的状态图.
如果添加了 volatile 变量写后, 线程中本地内存中共享变量就会置为失效的状态, 因此线程 B 再需要读取从主内存中去读取该变量的最新值. 下图就展示了线程 B 读取同一个 volatile 变量的内存变化示意图.
对 volatile 写和 volatile 读的内存语义做个总结.
线程 A 写一个 volatile 变量, 实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了 (其对共享变量所做修改的) 消息.
线程 B 读一个 volatile 变量, 实质上是线程 B 接收了之前某个线程发出的 (在写这个 volatile 变量之前对共享变量所做修改的) 消息.
线程 A 写一个 volatile 变量, 随后线程 B 读这个 volatile 变量, 这个过程实质上是线程 A 通过主内存向线程 B 发送消息.
3.2 内存语义的实现
我们知道, JMM 是允许编译器和处理器对指令序列进行重排序的, 但我们也可以用一些特殊的方式组织指令阻止指令重排序, 这个方式就是增加内存屏障. 我们先来简答了解下内存屏障, JMM 把内存屏障指令分为 4 类:
StoreLoad Barriers 是一个 "全能型" 的屏障, 它同时具有其他 3 个屏障的效果. 现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持). 执行该屏障开销会很昂贵, 因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush).
了解完内存屏障后, 我们再来看下 volatile 的重排序规则:
当第二个操作是 volatile 写时, 不管第一个操作是什么, 都不能重排序. 这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后.
当第一个操作是 volatile 读时, 不管第二个操作是什么, 都不能重排序. 这个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前.
当第一个操作是 volatile 写, 第二个操作是 volatile 读时, 不能重排序.
要实现 volatile 的重排序规则, 需要来增加一些内存屏障, 为了保证在任意处理器平台都可以实现, 内存屏障插入策略非常保守, 主要做法如下:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障.
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障.
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障.
在每个 volatile 读操作的后面插入一个 LoadStore 屏障.
需要注意的是: volatile 写是在前面和后面分别插入内存屏障, 而 volatile 读操作是在后面插入两个内存屏障
StoreStore 屏障: 禁止上面的普通写和下面的 volatile 写重排序;
StoreLoad 屏障: 防止上面的 volatile 写与下面可能有的 volatile 读 / 写重排序
LoadLoad 屏障: 禁止下面所有的普通读操作和上面的 volatile 读重排序
LoadStore 屏障: 禁止下面所有的普通写操作和上面的 volatile 读重排序
volatile 写插入内存屏障后生成的指令序列示意图:
volatile 读插入内存屏障后生成的指令序列示意图:
来源: https://www.cnblogs.com/yuanqinnan/p/11162682.html