Java volatile关键字详解
volatile关键字的语义:
1、定义为volatile的变量保持对所有线程的立即可见性,但并不意味着在并发下对volatile关键字修饰的变量进行操作是线程安全的。
2、volatile变量将禁用指令重排列优化
对于第一点的说明;
volatile变量能够保持对所有线程的立即可见性,这半句的实现基于两点:1、volatile变量的写操作总是立即同步到主内存中 ,2、volatile变量在使用前总是立即从主内存中刷新的。关于主内存的说明参照Java线程模型。
关于第一点的后半句“volatile变量在并发读写下并不是安全的“,即便volatile变量在使用前总是立即从主内存中拿到最新的值,但read这一对虚拟机来说保证了原子性的操作在底层的机器码实现上并不是原子的,也就是说在read这一过程中volatile变量在主内存上的值还是有可能发生改变,这就导致read操作拿到了一个过期的值。
对于第二点的说明:
package ryo; public class Test { private int flag = 0; private int[] arr; public static void main(String[] args) { new Test().twoThread(); } public void twoThread() { new Thread(new Runnable() { @Override public void run() { while(flag != 1) { } System.out.println(arr[4]); } }).start(); new Thread(new Runnable() { @Override public void run() { arr = new int[5]; flag = 1; } }).start(); } }
在上边这段代码中,可能因为底层的指令重排列优化导致在第二个线程中的int flag = 1发生在初始化数组之前,这就会导致第一个线程System.out.println(arr[4])抛出异常。
虽然实际上不太可能发生这个问题,即便是int flag = 1先于数组初始化发生,在线程一执行到打印语句之前,线程二也差不多把数组初始化的工作完成了,但理论上是由可能出错的。
这时候需要利用volatile禁用重排列的功能了。
给int flag加上volatile关键字,将保证int flag = 1 后于初始化语句执行,问题就解决了。
写到这里的时候突然想到一个问题,Java内存模型中存在一条既定规则,称之为程序次序规则,规定同一线程中书写在前面的操作先行发生于书写在后面的操作,那岂不是跟指令重排列优化相矛盾了吗?
照这条规则所言,上述例子中线程二的数组初始化不是一定发生在flag = 1之前吗?
反复翻了几遍书又在网络上找了相关问题的说法,对于这个问题的解释应该是这样:
程序次序规则只是保证同一方法的结果不变,也就是说具有逻辑关系的代码是有严格的先后顺序的,而不具有逻辑关系的代码是可以被重排列优化的。
举个例子说:
public void test() { String str1 = "abc"; String str2 = str1 + "def"; }
对于这两行代码,第二行就不可能被重排列优化调整到先于第一行之前执行,因为它们具有先后的逻辑关系,这也正是程序次序规则所保证的。
而如果改成这样:
public void test() { String str1 = "abc"; String str2 = "def"; }
则str2的初始化就有可能被放到str1之前了。