[深入理解Java虚拟机]原子性/可见性/有序性

原子性、可见性与有序性

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的,我们逐个来看一下哪些操作实现了这三个特性。

原子性(Atomicity)#

由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个, 我们大致可以认为,基本数据类型的访问、读写都是具备原子性的(例外就是long和double的非原子性协定,读者只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。 如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更 高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作。这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。

原子操作

  1. lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

可见性(Visibility)#

可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

上文在讲解 volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主内存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。 除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronizedfinal

同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操 作)”这条规则获得的。

而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通 过这个引用访问到“初始化了一半”的对象),那么在其他线程中就能看见final字段的值。

如代码清单 12-7所示,变量i与j都具备可见性,它们无须同步就能被其他线程正确访问。

public static final int i;
public final int j;
static {
	i = 0; // 省略后续动作
}
{
	// 也可以选择在构造函数中初始化
	j = 0; // 省略后续动作 
}

有序性(Ordering)#

Java内存模型的有序性在前面讲解volatile时也比较详细地讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的。前半句是指线程内似表现为串行的语义(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

介绍完并发中三种重要的特性,读者是否发现synchronized关键字在需要这三种特性的时候都可以作为其中一种的解决方案?看起来很“万能”吧?的确,绝大部分并发控制操作都能使用synchronized来完成。synchronized的“万能”也间接造就了它被程序员滥用的局面,越“万能”的并发控制,通常会伴随着越大的性能影响。

synchronized 如何实现 原子性、可见性与有序性

原子性#

原子性:既然同一时间只有一个线程去运行里面的代码,那么这个操作就是不能被其它线程打断的,所以这里天然就具有原子性了。

image

可见性#

我们继续来说synchronized是怎么保证可见性的?synchronized也是通过内存屏障保证可见性的。之前我们讲volatile的时候说过。

  • Load屏障保证volatile变量每次读取数据的时候都强制从主内存读取;
  • Store屏障每次volatile修改之后强制将数据刷新回主内存。

synchronized关键字也是通过内存屏障来保证可见性的。我们都知道sychronized底层是通过monitorenter的指令来进行加锁的、通过monitorexit指令来释放锁的。但是很多人都不知道的一点是,

  • monitorenter指令具有Load屏障的作用。也就是通过monitorenter指令之后,synchronized内部的共享变量,每次读取数据的时候被强制从主内存读取最新的数据。
  • monitorexit指令具有Store屏障的作用,也就是让synchronized代码块内的共享变量,如果数据有变更的,强制刷新会主内存。

这样通过这种方式,数据修改之后立即刷新回主内存,其他线程进入synchronized代码块后,使用共享变量的时候强制读取主内存的数据,上一个线程对共享变量的变更操作,它就能立即看到了。

image

有序性#

synchronizd是怎么保证有序性的,之前volatile通过内存屏障来保证有序性的,没错,synchronized也是通过内存屏障来保证有序性的。

之前我们讲过,四条禁止指令重排序的内存屏障,不记得话,要记得看一下之前的篇章哦,这4条禁止重排序的内存屏障分别为:

  • StoreStore屏障:禁止StoreStore屏障的前后Store写操作重排
  • LoadLoad屏障:禁止LoadLoad屏障的前后Load读操作进行重排
  • LoadStore屏障:禁止LoadStore屏障的前面Load读操作跟LoadStore屏障后面的Store写操作重排
  • StoreLoad屏障:禁止LoadStore屏障前面的Store写操作跟后面的Load/Store 读写操作重排

同样的道理啊,也是通过monitorenter、monitorexit指令嵌入上面的内存屏障;monitorenter、monitorexit这两条指令其实就相当于复合指令,既具有加锁、释放锁的功能,同时也具有内存屏障的功能。

image

作者:Esofar

出处:https://www.cnblogs.com/DCFV/p/18325066

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Duancf  阅读(25)  评论(0编辑  收藏  举报
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示