一篇文章看懂Java并发和线程安全(二)


声明:本文转载自https://my.oschina.net/u/1778239/blog/1613240,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

一、前言

   上一篇博客《一篇文章看懂Java并发和线程安全(一)》讲述了多线程中,程序总不能按照我们所看到的那样执行,必须保证共享数据的可见性和执行临界区代码的有序性,才能让多线程程序运行成我们想要的样子,本篇博客将继续深入讲解一个有序而又乱序的Java世界。

二、本博客重点导读

    1、工作内存与主内存的数据交换的细节

    2、指令重排序与内存屏障

    3、volatile、final、锁的内存语义

    4、as-if-serial、happens-before

三、进入细节

    1、主内存与工作内存交互协议

    JMM定义了8种基本操作来完成,主内存、工作内存和执行引擎之间的交互,分别是lock、unlock、read、load、use、assign、store、write,虚拟机的实现向程序员保证每一种操作都是原子的,不可分割,对于double和long类型的64为变量不做保证。了解了这些,有助于帮我们理解内存屏障。

    别看有8个操作,实际上是成对定义的连贯操作。我们具体来看怎么记忆。

    (1)、针对于主内存的单独操作lock和unlock

    lock:作用于主内存、把变量标示为线程独占

    unlock:作用于主内存、释放锁定状态

    (2)、主内存到工作内存的读交换

    read:作用于主内存,把主内存变量传递给工作内存

    load:作用于工作内存,把read操作传过来的值放入工作内存

    (3)、工作内存到主内存的写交换

    store:作用于工作内存,把工作内存变量传递给主内存

    write:作用于主内存,把store过来的值写入主内存变量

    (4)、工作内存和执行引擎的数据交换

    use:作用于工作内存,把工作内存变量传递给执行引擎

    assign:作用于工作内存,把执行引擎的值赋给工作内存变量

   上述的交互关系,可以用如下的图来表示:

    

    总体来说,工作内存和主内存的数据交换读写都是用两组操作来完成,而执行引擎和工作内存的数据交换由两个操作完成。当然,上述的8种操作必须满足一些规则,这里列举一些我认为重要的,例如:

    (1)、read和load、store和write必须同时出现‘

    (2)、对变量执行lock操作,会清空工作内存中缓存的该值,对变量执行unlock操作,必须先把值同步回主内存。

    废了这么大的篇幅,讲我们Java程序员并不关心的数据交换细节,是为了帮助我们理解后面的内存屏障,系好安全带,我们继续来看一个完全错乱的Java微观世界。

    2、乱序的Java世界

    在单线程的世界里,JMM向我们保证执行的正确性,那么我们可以逻辑的认为代码是根据我们编写的顺序执行。那么在多线程的世界里,站一个线程的视角看另一个线程,我们将完全看不清执行的顺序。并且也看不到对方执行结果。请看下面的代码:

public class ReorderTest { 	private int a = 0; 	private int b = 0; 	private int c = 2;  	public void write() { 		a = 1; 		b = 1; 	}  	public void read() { 		if (b == 1) { 			c = a; 		} 	} } 

    假设有两个线程A、B分别要执行write和read方法,A先进去执行、B随后执行,先抛开a、b线程可见性问题,假设a、b对线程立即可见。最后c值是多少?可能是1,可能是2,甚至可能是0。接下来具体分析一下为什么。

    站在B的视角看,它看不清a=1和b=1谁先执行,由于指令重排序,很可能b=1先执行,请看下表:

    

    站在B线程的视角,B线程中read方法里的代码是否会重排序呢,虽然这个方法的两句话存在依赖关系,JMM支持不改变结果的指令重排,JMM无法预先判断是否有其他线程在修改a的值,所以可能会重排,并且处理器会用猜测执行来重排。请看下表:

    

    指令重排序让线程看不清对方线程的执行顺序,也就是乱序的,那么会有哪些级别的指令重排序呢?有三种:编译器重排序、指令级重排序、内存级重排序。

    3、内存屏障

    指令重排序会导致多线程执行的无序,那么JMM会禁止特定类型的指令重排序,JMM通过内存屏障来禁止某些指令重排序,那么有哪些内存屏障呢?总共4类

    LoadLoad:前面的load会先于后面的load装载

    StoreStore:前面的store会先于后面的store执行,也就是保证内存可见性

    LoadStore:前面的load先于后面的store执行

     StoreLoad:前面的store先于后面的Load执行

    接下来分别看volatile、final、锁,都有哪些内存语义,加了哪些内存屏障。

    (1)、volatile

    对volatile变量的写操作,前面插入StoreStore屏障,防止和上面的写发生重排序;后面插入StoreLoad屏障,防止和后面的读写发生重排序。

    对volatile变量的读操作,后面会插入两个屏障,分别是LoadLoad、LoadStore,说白了就是,我是volatile变量,不管你下面的变量是读或者写,我都要先于你读。

    (2)、final   

    final本质上定义是final域与构造对象的引用之间的内存屏障。

   在构造函数对final变量的写人,与对构造函数对象引用的读,不能重排序,本质上是插入了storeStore屏障,保证对象引用被读之前,已经对final变量进行了写人。这里特别注意指针逃逸。

    读含有final变量的对象的引用,与读final变量不能指令重排序,插入loadload屏障,保证先读到对象引用,在读final变量的值,也就是只要对象构造完成,并且在构造函数中将final值写入,另外一个线程肯定可以读到,这是JMM的保证。

    (3)、锁

    ReentrantLock中 有个private volatile int state,本质上是用的volatile的内存语义,这里就省略讲了。

    4、as-if-serial、happens-before

    前面说这么多,指令重排序重排序,弄乱了Java程序,JMM提供volatile、final、锁来禁止某些指令重排序,那么记住这些重排序规则并非简单的事,JMM用另外一种好记的理论来帮助程序员记忆。

    as-if-serial:用通俗的话来解释一下,单线中,程序逻辑的以我们看到的顺序执行,这里只是可以逻辑的认为顺序执行,其实也会有不影响结果的指令重排,例如:

int i=1; int j=2; int a=i*j;

    这里i=1,j=1重排不影响结果,那么实际上JMM是允许的。  有了as-if-serial,在单线程中,程序员不用担心指令重排和内存可见性问题。

    happens-before

    happens-before保证如果A、B两个操作存在happens before关系,那么A操作的结果一定对B可见,有了可见性的保证,在加上正确的同步,就能写出线程安全的代码。JSR133定义了哪些天然的happens-before关系呢?请看下面:

    (1)、一个线程内,每个操作happens-before后面的操作

    (2)、unlock操作happens-before对这个这个锁的lock操作

    (3)、volatile写操作happens-before读操作

    (4)、线程的start方法happens-before此线程的所有其他操作

    (5)、线程所有操作happens-before对此线程的终止监测,例如,A线程调用B线程的join方法,如果join返回,那么B线程的所有操作必定完成,且B线程的所有操作的数据必定对A线程可见。

    (6)、传递性,A happens-before B、B happens-before C,那么A happens-before C

    最后总结一下,上一篇文章中围绕可见性和执行临界区代码的顺序性进行了说明,本篇文章,主要说的是可见性,就本质而言,加内存屏障,就是为了保证前面的操作对后面的操作可见,也就是我不能和你顺序弄乱了,我得看着你怎么执行,happens-before是JMM对Java程序员的承诺,记住这些规则,配合锁,必定线程安全。

    最后还有两句话

    在本线程内看,所有的操作都是有序的,这是as-if-serial的保证。

    一个线程看另一个线程,所有的操作都是无序的,主要是两方面所致,一方面是指令重排序,另一方面是不知道工作内存的值什么时候同步到主内存。

    

快乐源于分享。

   此博客乃作者原创, 转载请注明出处

 

 

本文发表于2018年01月26日 14:32
(c)注:本文转载自https://my.oschina.net/u/1778239/blog/1613240,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2022 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1