锁专题(三)工作5年了,竟然不知道 volatile 关键字( 二 )

使用 Lock synchronized 或者 AtomicInteger
volatile 能保证有序性吗volatile关键字禁止指令重排序有两层意思:

  1. 当程序执行到 volatile 变量的读操作或者写操作时 , 在其前面的操作的更改肯定全部已经进行 , 且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  2. 在进行指令优化时 , 不能将在对 volatile 变量访问的语句放在其后面执行 , 也不能把 volatile 变量后面的语句放到其前面执行 。
实例
  • 实例一
//x、y为非volatile变量//flag为volatile变量 x = 2;//语句1y = 0;//语句2flag = true;//语句3x = 4;//语句4y = -1;//语句5由于 flag 变量为 volatile 变量 , 那么在进行指令重排序的过程的时候 , 不会将语句3放到语句1、语句2前面 , 也不会讲语句3放到语句4、语句5后面 。
但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的 。
并且 volatile 关键字能保证 , 执行到语句3时 , 语句1和语句2必定是执行完毕了的 , 且语句1和语句2的执行结果对语句3、语句4、语句5是可见的 。
  • 实例二
//线程1:context = loadContext();//语句1inited = true;//语句2 //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);前面举这个例子的时候 , 提到有可能语句2会在语句1之前执行 , 那么久可能导致 context 还没被初始化 , 而线程2中就使用未初始化的context去进行操作 , 导致程序出错 。
这里如果用 volatile 关键字对 inited 变量进行修饰 , 就不会出现这种问题了 , 因为当执行到语句2时 , 必定能保证 context 已经初始化完毕 。
常见使用场景而 volatile 关键字在某些情况下性能要优于 synchronized ,
但是要注意 volatile 关键字是无法替代 synchronized 关键字的 , 因为 volatile 关键字无法保证操作的原子性 。
通常来说 , 使用 volatile 必须具备以下2个条件:
  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中
实际上 , 这些条件表明 , 可以被写入 volatile 变量的这些有效值独立于任何程序的状态 , 包括变量的当前状态 。
事实上 , 我的理解就是上面的2个条件需要保证操作是原子性操作 , 才能保证使用volatile关键字的程序在并发时能够正确执行 。
常见场景
  • 状态标记量
volatile boolean flag = false; while(!flag){doSomething();} public void setFlag() {flag = true;}
  • 单例 double check
public class Singleton{private volatile static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if(instance==null) {synchronized (Singleton.class) {if(instance==null)instance = new Singleton();}}return instance;}}JSR-133 的增强在 JSR-133 之前的旧 Java 内存模型中 , 虽然不允许 volatile 变量之间重排序 , 但旧的 Java 内存模型允许 volatile 变量与普通变量之间重排序 。
在旧的内存模型中 , VolatileExample 示例程序可能被重排序成下列时序来执行:
class VolatileExample {int a = 0;volatile boolean flag = false;public void writer() {a = 1;//1flag = true;//2}public void reader() {if (flag) {//3int i =a;//4}}}
  • 时间线
时间线:----------------------------------------------------------------->线程 A:(2)写 volatile 变量;(1)修改共享变量 线程 B:(3)读取 volatile 变量; (4)读共享变量在旧的内存模型中 , 当1和2之间没有数据依赖关系时 , 1和2之间就可能被重排序(3和4类似) 。
其结果就是:读线程B执行4时 , 不一定能看到写线程A在执行1时对共享变量的修改 。
因此在旧的内存模型中, volatile 的写-读没有监视器的释放-获所具有的内存语义 。
为了提供一种比监视器锁更轻量级的线程之间通信的机制 ,
JSR-133专家组决定增强 volatile 的内存语义:
严格限制编译器和处理器对 volatile 变量与普通变量的重排序 , 确保 volatile 的写-读和监视器的释放-获取一样 , 具有相同的内存语义 。
从编译器重排序规则和处理器内存屏障插入策略来看 , 只要 volatile 变量与普通变量之间的重排序可能会破坏 volatile 的内存语意 ,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止 。
volatile 实现原理术语定义
锁专题(三)工作5年了,竟然不知道 volatile 关键字文章插图
术语定义
原理那么 volatile 是如何来保证可见性的呢?
在 x86 处理器下通过工具获取 JIT 编译器生成的汇编指令来看看对 volatile 进行写操作 CPU 会做什么事情 。