Java笔记——Java并发3-Java内存模型2

Java笔记——Java并发3-Java内存模型2,第1张

Java笔记——Java并发3-Java内存模型2

JMM的设计思想:先保证正确性,然后尽量优化保证执行性能

JMM的设计意图

程序员对内存模型的要求,易于理解,易于编程,希望内存模型足够强,足有严格。

编译器和处理器为了尽可能地提高并行度,希望内存模型的约束越少越好。

对于会改变程序执行结果的重排序,JMM要求编译器和处理器禁止重排序;

对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求。

1.volatile的内存语义与实现 1.1 volatile写-读建立的happens before关系

volatile写happens before接下来的volatile读。一个volatile变量的写 *** 作,总是能被接下来的读 *** 作可见。

1.2 volatile的内存语义

volatile的写-读与锁的释放-获取有相同的内存效果,volatile写和锁释放有相同的内存语义,volatile读与锁获取有相同的内存语义。

volatile写的内存语义,当写一个volatile变量时,JMM会把当前线程中volatile写之前的共享变量从本地内存刷新到主内存中,同时也会把这个volatile变量刷新到主内存中。

volatile读的内存语义,当读取一个volatile变量时,JMM会把当前线程中volatile变量和volatile读后边的共享变量对应的本地内存置为无效,然后从主内存中读取最新的值到本地内存。

一个线程写volatile变量后,写之前的共享变量和这个volatile变量会对另一个接下来读这个volatile变量的线程立即可见。

1.3 volatile内存语义的实现

JMM为编译器指定了严谨的volatile重排序规则。

volatile写之前的 *** 作,不能重排序;

volatile读之后的 *** 作,不能重排序;

先volatile写,后volatile读,不能重排序。

是否重排序

第二个 *** 作

第一个 *** 作

普通读/写

volatile读

volatile写

普通读/写

No

volatile读

No

No

No

volatile写

No

No

为了实现volatile的内存语义,编译器在实现指令序列时,在适当的位置插入内存屏障指令来禁止特定的处理器重排序。

在每个volatile写之前插入StoreStore;禁止和之前的普通读/写重排序,禁止和之前的volatile读/写重排序

在每个volatile写之后插入StoreLoad;禁止和之后volatile读重排序

在每个volatile读之后插入LoadStore;禁止和之后的普通写/volatile写重排序

在每个volatile读之后插入LoadLoad;禁止和之后的普通读/volatile读重排序

JMM先采用保守策略,先插入全部内存屏障指令,然后编译器针对特定平台的CPU进行优化,去掉多余的内存屏障指令。

1.4 volatile内存语义被加强

旧版volatile的写-读没有锁释放-获取的内存语义;为了提供比锁轻量级的线程通信机制,对volatile的内存语义进行加强。注意直接通过读写volatile,实现不了锁的互斥性。

2.锁的内存语义与实现

锁可以实现临界区代码互斥执行。锁是Java并发编程中最重要的同步方式,同一个时间点只有一个线程能获取到锁。

2.1 锁释放-获取建立的happens before关系

锁释放happens before锁获取。

2.2 锁的内存语义

锁释放的内存语义,一个线程释放锁后,JMM会把这个线程对应的本地内存刷新到主内存中。

锁获取的内存语义,一个线程获取锁后,JMM会把这个线程对应的本地内存置为无效,使得访问临界区内的共享变量重新从主内存中获取。

2.3 CAS指令特性

(1)对内存的读改写 *** 作具有原子性;

(2)禁止CAS指令和之前的读、写 *** 作重排序,且禁止CAS指令和之后的读、写 *** 作重排序;

(3)把写缓冲区中的所有数据刷新到内存中。

CAS的同时具有volatile读和volatile写的内存语义。

2.4 可重入锁内存语义的实现

公平锁模式下

锁获取,核心 *** 作是读去volatile类型的state;

锁释放,核心 *** 作是写volatile类型的state。

非公平锁模式下

锁获取,核心 *** 作是CAS volatile类型的state;

锁释放,核心 *** 作是写volatile类型的state。

3.final域的内存语义

对于final域,编译器和处理器遵守的重排序规则:

(1)在构造函数内对一个对象的final域的写入,与随后把这个被构造的对象的引用赋值给一个引用变量,这两个 *** 作不能重排序;

(2)对于final域为对象引用类型时,在构造函数内对final域引用对象的成员的写入,与随后把这个被构造的对象的引用赋值给一个引用变量,这两个 *** 作不能重排序;

(3)首次读含有final域的对象的引用,与随后首次读这个对象的final域,这两个 *** 作不能重排序。

写final域的重排序规则保证,构造函数中的final域写入 *** 作和final域引用对象的成员写入 *** 作不会被重排序到构造函数之外,对象的引用对线程可见之前,对象的final域和final域引用对象的成员都已经正确初始化。

注意:对象的普通域,在构造函数中的写入 *** 作(初始化)可能会被重排序到构造函数之外。

读final域的重排序规则保证,在读一个对象的final域之前,一定会先读包含这个final域对象的引用。即对一个对象的final域的读取,读到的一定是初始化好的值。

注意:不要在构造函数中将this指针赋值给静态成员引用变量,避免造成final域的初始化 *** 作从构造函数中“溢出”,(实际上并不是final域的写 *** 作重排序到了构造函数外边,而是这个对象的引用过早的被其他线程可见)。

3.1 final域内存语义的实现

写final域的实现,在构造函数中,在写final域之后和构造函数return之前,插入StoreStore内存屏障。

读final域的实现,在读final域之前插入LoadLoad内存屏障。

4.happens-before 4.1 happens-before关系的定义

(1)如果一个 *** 作happens-before另一个 *** 作,那么第一个 *** 作的结果对第二个 *** 作的结果可见,第一个 *** 作排在第二个 *** 作前执行;

(2)如果两个 *** 作之间存在happens-before关系,如果重排序不改变执行结果,JMM允许这两个 *** 作发生重排。

as-if-serial原则保证,在单线程内程序的执行结果不会被重排序改变;happens-before原则保证,在多线程中,增加正确的同步措施后,程序的执行结果不会被重排序改变。

as-if-serial原则和happens-before原则具有相同的目标。两个的初衷都是在不改变程序执行结果的前提下,尽可能地提高程序(指令)执行的并行度。

4.2 happens-before规则

(1)程序顺序规则,在一个线程中,任意一个 *** 作happens-before后续任意多个 *** 作。

(2)监视器锁规则,监视器锁释放happens-before随后的监视器锁获取。

(3)volatile规则,对一个volatile域的写happens-before随后任意多个对这个volatile域的读。

(4)线程start规则,如果在线程A中执行启动线程B,即执行ThreadB.start(),则线程A中的ThreadB.start() *** 作happens-before线程B中的任意 *** 作。

(5)线程join规则,如果在线程A中等待线程B的执行结果,即执行ThreadB.join()并成功返回,则线程B的任意 *** 作happens-before线程A从ThreadB.join()执行返回。

(6)传递性规则,A *** 作happens-beforeB *** 作,B *** 作happens-beforeC *** 作,则A *** 作happens-beforeC *** 作。

参考资料

《Java并发编程的艺术》 机械工业出版社 方腾飞 魏鹏 程晓明

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4020887.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-10-22
下一篇 2022-10-22

发表评论

登录后才能评论

评论列表(0条)

保存