1. volatile 概述
volatile 相当于 synchronized 的弱实现,也就是说 volatile 实现了类似 synchronized 的语义,却又没有锁机制.它确保对 volatile 字段的更新以可预见的方式告知其他的线程.
2. volatile 语义
(1)Java 存储模型不会对volatile指令的操作进行重排序:这个保证对 volatile 变量的操作时按照指令的出现顺序执行的. volatile 可以禁止进行指令重排.
指令重排
是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的.指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性.
volatile 关键字禁止指令重排序有两层意思
-
程序执行到 volatile 修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行.
-
在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行.
//线程1:
context = loadContext(); //语句1 context初始化操作
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
因为指令重排序,有可能语句2会在语句1之前执行,可能导致 context 还没被初始化,而线程 2 中就使用未初始化的 context 去进行操作,导致程序出错.
这里如果用 volatile 关键字对 inited 变量进行修饰,就不会出现这种问题了.
(2)volatile变 量不会被缓存在寄存器中(只有拥有线程可见)或者其他对 CPU 不可见的地方,每次总是从主存中读取 volatile 变量的结果.也就是说对于 volatile 变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量.也就是在 happens-before 法则中,对一个 valatile 变量的写操作后,其后的任何读操作理解可见此写操作的结果.
尽管 volatile 变量的特性不错,但是 volatile 并不能保证线程安全的,也就是说 volatile 字段的操作不是原子性的,volatile 变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!
使用 volatile 关键字去修饰变量的时候,线程每次都会直接读取该变量并且不缓存它.这就确保了线程读取到的变量是同内存中是一致的
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法.但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了).
下面解释一下这段代码为何有可能导致无法中断线程.在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将 stop 变量的值拷贝一份放在自己的工作内存当中.
那么当线程2更改了 stop 变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对 stop 变量的更改,因此还会一直循环下去.
但是用 volatile 修饰之后就变得不一样了:
第一:使用 volatile 关键字会强制将修改的值立即写入主存.
第二:使用 volatile 关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是 CPU 的 L1 或者 L2 缓存中对应的缓存行无效).
第三:由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取.
那么在线程2修改 stop 值时(当然这里包括 2 个操作,修改线程 2 工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量 stop 的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值.
那么线程1读取到的就是最新的正确的值.
3. 应用 volatile 变量原则
(1)写入变量不依赖此变量的值,或者只有一个线程修改此变量
(2)变量的状态不需要与其它变量共同参与不变约束
(3)访问变量不需要加锁
4. volatile 原理和实现机制
下面这段话摘自《深入理解 Java 虚拟机》:
“观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令”
lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成.
2)它会强制将对缓存的修改操作立即写入主存.
3)如果是写操作,它会导致其他 CPU 中对应的缓存行无效.
5. synchronized 和 volatile 对比
- volatile 变量是一种稍弱的同步机制在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制.
- 从内存可见性的角度看,写入 volatile 变量相当于退出同步代码块,而读取 volatile 变量相当于进入同步代码块.
- 在代码中如果过度依赖 volatile 变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解.仅当 volatile 变量能简化代码的实现以及对同步策略的验证时,才应该使用它.一般来说,用同步机制会更安全些.
- 加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性,原因是声明为 volatile 的简单变量如果当前值与该变量以前的值相关,那么 volatile 关键字不起作用,也就是说如下的表达式都不是原子操作:count++、count = count+1.