volatile 的影响:可见性、有序性、原子性
可见性
可见性即对共享变量的修改立即对所有线程可见。
Java 内存模型规定每个线程都有自己的工作内存用于存储变量的副本,而且不能直接读取主内存或其他线程的工作内存。因此,一个线程对变量修改后其他线程若不刷新副本,将看不到变量被修改。以下面的代码为例,子线程很可能将永远不会停止
1 |
|
jcstree 的例子BasicJMM_04_Progress.java
有序性
Java 编译器在生成字节码时可能会改变指令顺序以提高性能,指令重排的过程仅保证单线程环境下的执行结果一致,而在多线程下可能就表现出乱序。因为存在有序性问题的代码往往都有可见性问题,并没有很好的示例代码来单独演示有序性…一个常见的例子是懒加载的单例模式
1 |
|
jcstree 的例子BasicJMM_08_Finals.java
对象的创建大致分为三个步骤:1.申请内存、2.初始化对象、3.将变量指向内存,经编译器指令重排后可能变成1.申请内存、2.将变量指向内存、3.初始化对象。当一个线程将内存分配给对象但还未完成初始化时,其他线程会认为这个对象已经完全创建好了,但访问时很有可能会产生异常。
double、long 的原子性
Java 没有强制要虚拟机保证 64 位数据类型的读写操作的原子性,也就是说虚拟机可以将 long 变量分两次写入数值。以下代码来自stackoverflow
1 |
|
在 32 位的 OpenJDK8 得到以下输出
1 |
|
jcstree 的例子BasicJMM_02_AccessAtomicity.java
volatile 关键字强制虚拟机保证 64 位数据类型的原子性。不过商用虚拟机基本都已经实现原子操作,不需要额外的声明
总结
《Java 并发编程实战》给出了 volatile 变量使用规则:
- 对变量的写入操作不依赖变量的当前值,或能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
一点解释:
- 如果对变量的写入依赖当前值,那么写入操作实际有三步:“读取-修改-写入”,而 volatile 不保证整个操作的原子性
- 如果为了使类的状态保持一致,在修改一个 volatile 变量还必须修改其他变量,那么这个操作依然有多个步骤
- 锁能保证可见性,不需要额外的 volatile 修饰
Ps:第一个示例中的 doSomething()
替换成 System.out.printf()
能正常终止进程,原因见这里 loop-doesnt-see-changed-value-without-a-print-statement
2018-06-05 更新
示例一的代码加 -Xint 参数强制虚拟机以解释方式运行也能正常终止,所以问题并不在可见性上…Jit 部分还需要我再学习一个
参考:
《深入理解 Java 虚拟机》
double-checked-locking-with-delay-initialization