Java 的 volatile 关键字经常见到, 但是错误理解其中的原理和机制,
少不了会埋下许多炸弹, 有时可能造成严重损失.
线程内存模型
为什么要说这个呢? 因为理解 Java 的线程内存模型有助于我们更好地理解 volatile 的工作方式.
参考另一篇博客
volatile 作用
作用1. 可见性
我们为什么使用 volatile, 相信大部分人都是为了可见性, 保证变量对所有线程的可见性.
比如一条线程修改了变量值, 其他线程可以马上得知.
某些情况来说, volatile 可以避免一致性问题, 因为在使用前都会刷新,
但是在 比如自增操作的情况下, 就会有并发一致性的问题.
我们都知道i++
, 转成字节码是有4个指令:
getstatic |
假设在 getstatic
时, volatile 保证了此时的 i 值是最新的,
但是直到 pustatic
同步回主内存的这个操作期间,
可能有其他线程更新 i 值, 导致数据不一致.
需要注意的是, 这些字节码指令也不一定
是原子操作(可用 -XX:+PrintAssembly
查看汇编指令).
因此, volatile 只能保证可见性, 而非一致性. 有些场景我们还是需要用锁来保证原子性(或者使用 CAS).
其他方法实现可见性:
除了 volatile, 还有两个关键字能实现可见性: synchronized
和 final
.
synchronized: 在 unlock 前, 会把数据同步回主内存(store + write).
final: 构造器初始化完成后, 变量的值就固定了.
作用2. 禁止指令重排序优化
大多数现代微处理器都会采用将指令乱序执行的方法(out-of-order execution,简称OoOE或OOE),
在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待,
这样可以大大提高执行效率。JIT编译器也会做指令重排序操作4,即生成的机器指令与字节码指令顺序不一致。
可以看下面的两个例子:
例子1: 提前执行
// 初始化 |
上面代码中的initialized
用了volatile修饰, 若非如此, initialized = true
可能被提前执行.
因为机器级别的重排序优化, 把该代码的汇编指令提前执行了, 使用volatile就可以避免这种情况的发生.
例子2: 双重检查
看过GOF的<设计模式>
的同学, 应该记得单例模式那部分有提到双重检查延迟初始化的代码, 大概是这样的:
public class VolatileSingleton { |
其中 instance
用了 volatile
修饰, 为什么呢? 我们看一下打印出来的汇编代码 :
0x00007f512d108fae: mov 0x20(%rsp),%rsi |
其中 lock addl $0x0,(%rsp)
操作 (如果去掉volatile
的话, 将不会有这一句),
是把 rsp 的内容加 0, 这个相当于什么都没做, 这个有什么作用呢?
其实这个和 lock 搭配, 相当于一个内存屏障
, 他会把前面的修改内容同步到内存, 同时通知到缓存子系统.
(那为什么不用nop
呢? 因为 lock 不允许和 nop 搭配使用) lock操作会把高速缓存写入内存,
并促发其他CPU或内存去 invalidate 自己的 cache .
参考
https://en.wikipedia.org/wiki/Memory_barrier
http://tech.meituan.com/java-memory-reordering.html
周志明 - [深入理解Java虚拟机.JVM高级特性与最佳实践]