Java虚拟机高效并发之先行发生原则

上两篇关于JMM和volatile变量特殊规则的博文中,都提起了happens-before原则。之前关于线程池ThreadPoolExecutor类的学习中也浅度的介绍了一下先行发生原则。

之前在学习Java内存模型时,了解到Java内存模型中的内存交互规则,以及volatile关键字的特殊规则,基本就能判断出内存访问在并发环境下的安全,但是这种定义相当严谨又十分繁琐,实践起来很麻烦,所以JMM中定义一种等效的判断原则——先行发生原则。

如果Java内存模型中的所有操作的有序性都仅仅依靠volatile关键字和synchronized来完成,那么有一些操作就会变得很繁琐,但是在实际并发编码过程中,并没有感觉这一点,就是因为Java内存模型中的想发生原则。它是判断数据是否存在竞争、线程是否安全的主要依据。

先行发生是Java内存模型中定义的两项操作间的偏序关系。如果说操作A先行发生操作B,那么在操作B发生之前,操作A产品的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。举个伪代码例子,

//线程A中执行
i = 1;

//线程B中执行
j = i;

//线程C中执行
i = 2;

假设线程A操作先行发生于线程B操作,那么可以确定在线程B操作执行后,变量j的值一定等于1。这个结果的依据有2个,一是根据先行发生原则,线程A操作的结果会被线程B观察到;二是线程C还没有“登场”,线程A操作结束之后没有其他线程会改变变量i的值。现在考虑线程C,继续保持线程A操作和线程B操作的先行发生关系,而线程C出现在线程A和线程B的操作之间,但是线程C操作与线程B操作不存在先行发生关系,此时j的值是多少?答案不确定,可以是1也可以是2,因为线程C对变量i的影响可能会被线程B观察到,也可能不会,所以不具备多线程安全性。

列举几条JMM中“天然的”先行发生关系,这些先行发生关系无须任何同步器(AQS,AbstractQueuedSynchronizer)协助就已经存在,可以直接在代码中应用,如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排。

程序次序规则:在一个线程内,按照程序代码的顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调是同一个锁,而“后面”指时间上的先后顺序。

volatile变量规则:对于一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样指的是时间上的先后顺序。

④线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

⑤线程终止规则:线程中所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()方法的返回值等手段检测到线程已经终止执行。

⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

⑦对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C的结论。

Java语言中无须任何同步手段保障就能成立的先行发生规则就只有上面这些了,常见的就是前三个规则:程序次序规则(同一个线程)、管程锁定规则、volatile变量规则,内存操作间是否具备顺序性,对于共享变量的操作来说,就是线程是否安全,先行发生原则中,“时间先后”与“先行发生”之间并不存在特定的关系。

private int value = 0;

public void setValue(int value){
	this.value = value;
}

public int getValue(){
	return this.value;
}

如果存在线程A和线程B,线程A先调用了setValue(1),然后线程B调用了同一个对象的getValue()方法,那么线程B收到的结果是什么?

我们根据Java语言中“天然的”先行发生规则来推导一下两个操作间的有序性(线程是否安全),由于两个操作分别由线程A和线程B调用,不在一个线程中,所以“程序次序规则”在此处不适用。由于没有同步块,自然没有lock操作,所以“管程锁定规则”也不适用,value变量为普通变量,所以“volatile变量规则”也不适用,后面的线程启动、终止、中断,对象终结规则都不适用,所以无法保证操作间的顺序性,无法确定线程B中“getValue()”操作的返回结果,换句话说,这里操作是线程不安全的。

根据先行发生原则,有2种方法解决这个问题,一是将get/set方法定义为synchronized方法,即满足管程锁定规则,二是将变量value用volatile关键字修饰,则满足了volatile变量的特殊规则。这2种方法保证了操作间的顺序性,是线程安全的。

至此,Java内存模型的知识基本都已经了解了,回忆一下,由操作系统的内存模型(主内存与高速缓存Cache)类比到Java内存模型(主内存和工作内存),内存间的交互规则,由交互规则可以用来保证操作的顺序性来判断线程安全性等效简化到happens-before原则,volatile关键字的两种语义及其特殊规则到64位变量(long和double)的非原子性协定(基本不会发生)。其实学习中我们会发现,内存交互,volatile变量的特殊规则,先行发生原则,它们之间是相辅相成的。最后JMM中的三种特性:可见性、原子性、有序性保证了内存交互操作的线程安全。

你可能感兴趣的:(java虚拟机)