并发中的volatile

1. 概述

因为线程有本地内存的存在, 一个线程修改的共享变量不会及时的刷新到主内存中, 使得另外一个线程读取共享变量时读取到的仍旧是旧值, 就致使了内存可见性问题. 如今volatile就能够解决这个问题, 为何能解决内存可见性问题呢? 本文就来揭开volatile的神秘面纱.sql

2. volatile的特性

理解volatile特性的一个好方法就是把对volatile单个变量的读/写, 当作是使用同一个锁对单个变量的读/写作了同步.架构

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性, 这意味着对一个volatile变量的读, 老是能看到(任意线程)对这个volatile变量最后的写入.并发

锁的语义决定了临界区代码的执行具备原子性. 这意味着, 即便是64位的long型和double型变量, 只要它是volatile变量, 对该变量的读/写就具备原子性. 若是是多个volatile操做或相似于volatile++这种复合操做, 这些操做总体上不具备原子性.app

简言之, volatile变量自身具备如下特性.分布式

  • 可见性: 对一个volatile变量的读, 老是能看到任意线程对这个volatile变量最后的写入.
  • 原子性: 对任意单个volatile变量的读/写具备原子性, volatile变量的复合操做不具备原子性.

3. volatile写-读的内存语义

volatile写的内存语义

当写一个volatile变量时, JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中.高并发

volatile读的内存语义

当读一个volatile变量时, JMM会把线程对应的本地内存中的共享变量值置为无效, 线程接下来将从主内存中读取共享变量.性能

volatile内存语义总结

  • 线程A写一个volatile变量, 实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所作修改的)消息.
  • 线程B读一个volatile变量, 实质上是线程B接收了以前某个线程发出的(在写这个volatile变量以前对共享变量所作修改的)消息.
  • 线程A写一个volatile变量, 随后线程B读这个volatile变量, 这个过程实质上是线程A经过主内存向线程B发送消息.

4. volatile内存语义的实现

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

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

从图中能够看出:优化

  • 当第二个操做是volatile写时, 无论第一个操做是什么, 都不能重排序. 这个规则确保volatile写以前的操做不会被编译器重排序到volatile写以后.
  • 当第一个操做是volatile读时, 无论第二个操做是什么, 都不能重排序. 这个规则确保volatile读以后的操做不会被编译器重排序到volatile读以前.
  • 当第一个操做是volatile写, 第二个操做是volatile读时, 不能重排序.

为了实现volatile的内存语义, 编译器在生成字节码时, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序. 对于编译器来讲, 发现一个最优布置来最小化插入屏障的总数几乎不可能. 为此, JMM采起保守策略. 下面是基于保守策略的JMM内存屏障插入策略.spa

  • 在每一个volatile写操做的前面插入一个StoreStore屏障.
  • 在每一个volatile写操做的后面插入一个StoreLoad屏障.
  • 在每一个volatile读操做的后面插入一个LoadLoad屏障.
  • 在每一个volatile读操做的后面插入一个LoadStore屏障.

上述内存屏障插入策略很是保守, 但它能够保证在任意处理器平台, 任意的程序中都能获得正确的volatile内存语义.

在实际执行时, 只要不改变volatile写-读的内存语义, 编译器能够根据具体状况省略没必要要的屏障. 举个例子.

有以下代码:

public class Demo {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;
    void readAndWrite() {
      int i = v1; // 第一个volatile读
      int j = v2; // 第二个volatile读
      a = i + j; // 普通写
      v1 = i + 1; // 第一个volatile写
      v2 = j * 2; // 第二个 volatile写
    }
}

针对readAndWrite()方法, 理论上生成字节码时会以下:

int i = v1; // volatile读后面插入LoadLoad和LoadStore屏障
LoadLoad; // 确保v1的装载先于后续装载指令
LoadStore; // 确保v1的加载先于后续存储指令

int j = v2; // volatile读后面插入LoadLoad和LoadStore屏障
LoadLoad; // 确保v2的装载先于后续装载指令
LoadStore; // 确保v2的加载先于后续存储指令

a = i + j; // 普通读写无屏障

StoreStore; // 确保以前的存储指令要先于v1的存储
v1 = i + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令

StoreStore;  // 确保以前的存储指令要先于v1的存储
v2 = j + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令

因为不一样的处理器有不一样的"松紧度"的处理器内存模型, 内存屏障的插入还能够根据具体的处理器内存模型继续优化, 以X86处理器为例, 处理最后的StoreLoad屏障外, 其它的屏障都会被省略. X86处理器仅会对写-读操做作重排序, 不会对读-读, 读-写和写-写操做作重排序. 所以X86处理器会省略掉这3中操做类型对应的内存屏障. 因此在X86处理器中, JMM仅需在volatile写后面插入一个StoreLoad屏障便可实现volatile写-读的内存语义.欢迎工做一到五年的Java工程师朋友们加入Java架构交流圈:874811168 圈内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!

下面是X86处理器优化以后的内存屏障

int i = v1; // volatile读后面插入LoadLoad和LoadStore屏障
// LoadLoad; // 确保v1的装载先于后续装载指令
// LoadStore; // 确保v1的加载先于后续存储指令

int j = v2; // volatile读后面插入LoadLoad和LoadStore屏障
// LoadLoad; // 确保v2的装载先于后续装载指令
// LoadStore; // 确保v2的加载先于后续存储指令

a = i + j; // 普通读写无屏障

// StoreStore; // 确保以前的存储指令要先于v1的存储
v1 = i + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令

// StoreStore;  // 确保以前的存储指令要先于v1的存储
v2 = j + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令

5. JSR-133为何要加强volatile的内存语义

JSR-133也就是在JDK1.5中加入的.

在JSR-133以前的旧Java内存模型中, 虽然不容许volatile变量之间重排序, 但旧的Java内存模型容许volatile变量与普通变量重排序.

在旧的内存模型中, 当1和2之间没有数据依赖关系时, 1和2之间就可能被重排序(3和4相似). 其结果就是: 读线程B执行4时, 不必定能看到写线程A在执行1时对共享变量的修改.

所以, 在旧的内存模型中, volatile的写-读没有锁的释放-获所具备的内存语义. 为了提供一种比锁更轻量级的线程之间通讯的机制, JSR-133专家组决定加强volatile的内存语义: 严格限制编译器和处理器对volatile变量与普通变量的重排序, 确保volatile的写-读和锁的释放-获取具备相同的内存语义. 从编译器重排序规则和处理器内存屏障插入策略来看, 只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义, 这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止.

6. 总结

volatile能保证内存可见性正是经过内存屏障来实现的, 而且不一样的编译器对内存屏障的支持不一样, 可是因为大多数处理器都使用了写缓冲区, 因此大多数处理器都支持StoreLoad屏障.

------------------------------------------- 不要由于知识简单就忽略, 不积跬步无以致千里.

相关文章
相关标签/搜索