java内存模型
2019-08-04 01:00:55 32 举报
jvm如何保证线程并发安全的
作者其他创作
大纲/内容
变量
第一条
volatile阻止指令重排序的原理:在生成的目标代码时,插入了一类特殊指令(内存屏障指令),该指令的作用:不能把后面的指令重排序到该命令之前的位置。这里需要注意得是该指令可能会插入多条,以实现和程序代码一致的执行顺序。
synchronized代码块儿里面的变量都实现了内存可见性。内存可见性的底层是CPU的指令实现的。volatile修饰的是变量,它的作用也是实现内存可见性,底层用的用的同一个CPU指令。可以这样理解:synchronized里面的变量都是volatile修饰的。
什么叫内存可见性?什么叫寄存器可见性?数据在哪里存在呢?只在内存吗?不是的。数据在内存中存在,但是当用的时候会加载到CPU的寄存器里面。内存和寄存器是两个地方,从而出现了新的名词:内存可见性和寄存器可见性。数据的流动过程是:内存->寄存器->计算器很多时候,数据从内存地址读取到寄存器里面,后面的计算过程中CPU就一直使用寄存器里面的值,即是内存地址上的值发生变化,CPU也不知道,CPU此时就是井底之蛙,而变量此时可以称为:寄存器可见性。但是当变量被volatile修饰之后,CPU就不再偷懒,只要用到数据,它都会越过寄存器,直接从内存中读取,然后再在计算器中计算,最后返回给内存,这个时候变量就不在寄存器里面停留,就当寄存器不存在一样,这就称为内存可见性。
上面的代码,是对的,如果变量不用volatile修饰,这段代码就会出问题,因为指令重排序,线程A可能还没有加载完配置文件,就把flag赋值成了true,线程A这样做,对其本身是可行的,但可会导致线程B出问题。
volatile
工作内存
Lock(锁定):作用于主内存中的变量,把一个变量标识为一条线程独占的状态。Unlock(解锁):作用于主内存中的变量,把一个处于锁定状态的变量释放出来,之后可被其它线程锁定。Read(读取):作用于主内存中的变量,把一个变量的值从主内存传输到线程的工作内存中。Load(加载):作用于工作内存中的变量,把read操作从主内存中得到的变量的值放入工作内存的变量副本中。Use(使用):作用于工作内存中的变量,把工作内存中一个变量的值传递给执行引擎。Assign(赋值):作用于工作内存中的变量,把一个从执行引擎接收到的值赋值给工作内存中的变量。Store(存储):作用于工作内存中的变量,把工作内存中的一个变量的值传送到主内存中。Write(写入):作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
read
Java中的内存模型
use
缓存一致性协议(内存模型)
高速缓存
第二条特性,有点难以理解,我自己也不知道是否理解正确,能够自圆其说,但不知道是不是自欺欺人。处理器在执行命令的顺序和程序代码的顺序往往是不一致的,原因在于处理器中的物理元间往往有多个,比如有两个加法器,这时就可以两个指令同时运行,至于哪个先完成,不确定,但会保证所以依赖之前命令执行结果的地方都能获取到正确的结果,这种模式叫做\"线程内表现为串行的语义\"。虽然,在当前线程中观察,命令表现出来的现象为\"串行执行\",但对别的线程来说,就是当前线程的执行就是无序的。这会导致一个比较隐晦的bug。示范代码如下:
临时变量
assign
load
其实Java内存模型就是想定义出一套规范, 正确的实现并发过程中的 原子性、可见性与有序性 问题。原子性(Atomicity):由Java内存模型来直接保证的原子性变量包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大方位的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块--synchronized关键字,因此在synchronized块之间的操作也具备原子性。可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final.同步快的可见性是由“对一个变量执行unlock操作前,必须先把此变量同步回主内存”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把\"this\"的引用传递出去,那么在其他线程中就能看见final字段的值。有序性(Ordering):Java内存模型的有序性在前面讲解volatile时也详细的讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的:如果在一个线程中观察另外一个线程,所有的线程操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。
store
计算机中的内存模型
unlock
其实就是定义如何对内存或高速缓存进行读写访问,而不出现缓存一致性问题。具体来说,就是 主内存(虚拟机内存的一部分)、工作内存、执行引擎 数据如何交互。这里的执行引擎,具体来说就是虚拟机中的线程,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。可以把线程的工作内存理解为高速缓存。
对于volatile变量,每个线程对该变量实施的动作有以下附加的规则,假定t表示一个线程,v,w表示volatile变量, 1: 只有当线程t对变量v执行的前一个动作是load的时候,线程t才能对变量v执行use操作,并且,只有当线程t对变量v执行的后一个动作是use的时候,线程t才能对v执行load操作,这样就保证了其它线程对变量 v修改后,线程使用时必须先去主内存加载。 2:只有当线程t对变量v执行的前一个动作是assign时候,线程t才能对变量v执行store操作,并且只有当线程t对变量v执行的后一个动作是store时,线程t才能对v执行assign动作,这样保证修改后会立即写会主内存 3: 假定动作a是线程t对变量v实施的use或assign动作,假定动作f是和动作a相关联的load或store动作,假定动作p是和动作f相对应的对变量v的read或write动作,类似的,动作b是线程t对变量w实施的use或assign动作,动作g是和动作b相关联的load或store动作,假定动作q是和动作g相应的对变量w的read和write动作。如果a先于b,那么p先于q。也即对变量v、w写回主内存的顺序与程序代码对v、w赋值先后顺序一致。前两条规则,确保了内存一致性。后一条,确保了volatile修饰的变量不会被指令重排序优化。
JVM内存模型规范:执行上述8种操作,必须满足如下规则:
volatile boolean flag = false;//线程A加载配置文件flag = true;//线程Bwhile(!flag){ sleep();}使用配置文件中的配置
write
执行引擎
volatile是java虚拟机提供的最轻量级的同步机制。理解volatile如何实现内存可见性的,就差不多理解jvm如何实现并发安全的了。
第二条
原子性、可见性与有序性
内存栅栏即不同内存间的间隔,比如,高速缓存与主内存之间就有一个逻辑上的栅栏。
参考:深入理解Java虚拟机
如何理解这两个特性
lock
volatile有具备两种特性:第一保证此变量对所有线程的可见性,这里的\"可见性\"指的是一条线程修改了这个变量的值,新值对其他线程来说是可以立即得到的。从内存的角度来理解就是,当一个线程把值写到工作缓存,被立即刷新到了主内存中;执行引擎用到这个变量的时候需要直接从主内存中取,而不是使用工作内存中的值。第二是禁止指令的重排序优化。
先行发生原则
主内存
由于指令重排序,cpu时间片分配原则,会导致\"时间上先发生的指令\",不能确保一定\"先行发生(先行执行)\"。这和自然事实有所违背,所以Java内存模型,规定了一些“天然的”先行发生关系,如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。 a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。 b.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。 c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。 d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。 e.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。 f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。 g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。 g.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。 一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生 “呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与happens-before原则之间基本没有什么关系,所以衡量并发安全问题一切必须以happens-before 原则为准。
cpu内核
Java内存模型是如何规定volatile的规则的
JVM内存模型规范:实现以下8种操作,并保证原子性
0 条评论
下一页
为你推荐
查看更多