Java语言中有一个happen-before规则,它是Java内存模型中定义的两项操作之间的偏序关系。如果操作A happen-before 于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
举例来说,假设存在如下三个线程,分别执行对应的操作:
线程A中执行如下操作:i=1
线程B中执行如下操作:j=i
线程C中执行如下操作:i=2
假设线程A中的操作”i=1“ happen—before线程B中的操作“j=i”,那么就可以保证在线程B的操作执行后,变量j的值一定为1,即线程B观察到了线程A中操作“i=1”所产生的影响;现在,我们依然保持线程A和线程B之间的happen—before关系,同时线程C出现在了线程A和线程B的操作之间,但是C与B并没有happen—before关系,那么j的值就不确定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,所以不具备线程安全性。
- 程序次序规则:在一个单线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
- 管理锁规则:一个unlock操作happen—before后面对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
- 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
- 线程终止规则:线程的所有操作都happen—before对此线程的终止检测。
- 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断事件的发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
- 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
1 –> ”时间上执行的先后顺序“与”happen—before“之间有何不同呢?
private int value = 0;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
假设存在线程A和线程B,线程A先(时间上)调用了setValue(3)操作,然后(时间上)线程B调用了同一对象的getValue()方法,那么线程B得到的返回值一定是3吗?
对照以上八条happen—before规则,发现没有一条规则适合于这里的value变量,从而我们可以判定线程A中的setValue(3)操作与线程B中的getValue()操作不存在happen—before关系。因此,尽管线程A的setValue(3)在操作时间上先于操作B的getvalue(),但无法保证线程B的getValue()操作一定观察到了线程A的setValue(3)操作所产生的结果,也即是getValue()的返回值不一定为3。这里的操作不是线程安全的。
因此,”一个操作时间上先发生于另一个操作“并不代表”一个操作happen—before另一个操作“。
解决方法:可以将setValue(int)方法和getValue()方法均定义为synchronized方法,也可以把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。
2 –> 操作A happen—before操作B,是否意味着操作A在时间上先与操作B发生?
x = 1;
y = 2;
假设同一个线程执行上面两个操作:操作A:x=1和操作B:y=2。根据happen—before规则的第1条,操作A happen—before 操作B,但是由于编译器的指令重排序(也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。操作A在时间上有可能后于操作B被处理器执行,但这并不影响happen—before原则的正确性。
因此,”一个操作happen—before另一个操作“并不代表”一个操作时间上先发生于另一个操作“。
DCL即双重检查加锁
public class LazySingleton {
private int someField;
private volatile static LazySingleton instance;
//no volatile
private LazySingleton() {
this.someField = new Random().nextInt(200)+1; // (1)
}
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
public int getSomeField() {
return this.someField; // (7)
}
}
问题的关键在于尽管得到了Singleton的正确引用,但是却有可能访问到其成员变量的不正确值。
对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。
public class Singleton {
private Singleton() {}
// Lazy initialization holder class idiom for static fields
private static class InstanceHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getSingleton() {
return InstanceHolder.instance;
}
}
- 把volatile写和volatile读这两个操作综合起来看,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前,所有可见的共享变量的值都将立即变得对读线程B可见。
- volatile屏蔽指令重排序的语义
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量。此处的变量主要是指共享变量,存在竞争问题的变量。
Java内存模型规定所有的变量都存储在主内存中,而每个线程还有自己的工作内存,线程的工作内存保存了该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取,赋值等)都必须在工作内存中,而不能直接读写主内存中的变量。
volatile变量依然有共享内存的拷贝,但是由于它特殊的操作顺序性–规定从工作内存中读写数据前,必须先将主内存中的数据同步到工作内存中,所以看起来如同直接在主内存中读写访问一般,因此这里的描述对于volatile也不例外。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
Java内存模型中定义了以下8种操作来完成主内存与工作内存之间交互的实现细节:
1. lock(锁定):作用于主内存的变量,它把一个变量标识为一个线程独占的状态。
2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程访问
3. read(读取):作用于主内存的变量,它把一个变量的值从主内存中传输到工作内存中,以便随后的load动作使用。
4. load(加载):作用于工作内存的变量,它把read操作从主内存中得到的变量放入工作内存的变量副本中。
5. use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用该变量的字节码指令时将会执行这个操作。
6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存中的变量。
7. store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递给主内存中,以后随后的write操作使用
8. write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
一些规则:
1、不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了read和assign操作。
5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
final类型的域是不能修改的,final域能确保初始化过程的安全性,被final修饰的成员变量在构造器中一旦被初始化完成,并且构造器没有把”this”的引用传递出去,那么在其他线程中就能看到final成员变量的值。
Java内存模型要求lock、unlock、read、load、assign、use、store和write这8个操作都具有原子性,但是对于64位的数据类型long和double。允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的读写操作来进行。目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此在编码时,不需要将long和double变量专门声明为volatile。