-
原子性、可见性、有序性
java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。
(1)原子性
由java内存模型来直接保证的原子性操作包括read、load、use、assign、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(long和double的非原子性协定)。
如果应用场景需要一个更大范围的原子性保证,java内存模型还提供了lock和unlock操作来满足这种需求,尽管JVM未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到java代码块中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。
(2)可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。java内存模型是通过在变量修改之后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
除了volatile之外,synchronized和final关键字也可以实现可见性。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存”这条规则获取的,而final关键字的可见性是指:被final修饰的变量在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就能看见final字段的值。
(3)有序性
java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的(线程内表现为串行的语义);如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序现象和工作内存与主内存同步延迟现象)。
java通过volatile和synchronized两个关键字来保证线程间操作的有序性。volatile关键字本身禁止指令重排序(通过lock操作),而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获取的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
- 先行发生原则
如果java内存模型中的所有有序性都紧靠volatile和synchronized来完成,那么有一些操作将会变得很繁琐,但是我们在编写java代码的时候并没有察觉到这一点,这是因为java中有一个“先行发生”的原则。
这个原则是判断数据是否存在竞争、线程是否安全的主要依据。
先行发生是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括了修改了内存中共享变量的值、发送了消息、调用了方法等。
如下:
//A线程执行的操作
int i = 1;
//B线程执行的操作
int j = i;
//C线程执行的操作
i = 2;
暂不考虑C线程,假设A线程先行发生于B线程,那么可以确定B线程的操作执行完之后,变量j的值为1。这是因为第一,根据先行发生原则,A线程操作过后变量i的值可以被B线程观察到。第二,C线程还没饿执行,A线程执行结束之后没有其他线程会修改变量i的值。
再考虑C线程,依然保持A线程先行发生于B线程,而C线程出现在A线程和B线程的操作之间,但是线程C与线程B没有先行发生关系,那变量j的值是多少呢?可能是1,也可能是2。因为C线程对变量i的影响可能被B线程观察到,也可能观察不到。这时B线程就可能读取到过期数据,不具备多线程的安全性。
java中存在一些天然的先行发生关系,这些先行发生关系无须任何协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在这些关系中,并且无法从这些关系中推导出来的话, 它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
(1)程序次序规则:在一个线程内,按照程序代码顺序执行,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
(2)管程锁定规则:一个unlock操作先行发生与后面的对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
(3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
(4)线程启动规则:Thread对象的start方法先行发生与此线程的每一个动作。
(5)线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()方法的返回值等手段检测到线程已经终止执行。
(6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
(7)对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
(8)传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
如下:
private int value = 0;
public int getValue(){
return value;
}
public void setValue(){
this.value = value;
}
假设存在线程A和线程B,线程A先(时间上的先后)调用了setValue(1),然后线程B调用了同一个对象的getValue()方法,那么线程B接收到的返回值是什么?
一次分析先行发生原则中的各项规则:由于两个方法分别由线程A和线程B调用,不在一个线程中,所以程序次序规则在这里不适用;由于没有同步块,所以也不考虑管程锁定规则;value变量没有被volatile修饰,所以也不考虑volatile变量规则;线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起。因此虽然线程A在操作时间上先于线程B,但是无法确定线程B的接收结果,也就是说,这里是线程不安全的。
如何解决这个问题呢?第一,可以使用synchronized关键字修饰get和set方法,这样就可以使用管程锁定规则。第二,可以用volatile修饰value变量,这样就可以使用volatile变量规则。
因此,一个操作时间上的先发生不代表这个操作会先行发生,同样一个操作先行发生,那这个操作并不一定是时间上的先发生。