Java并发编程_Java内存模型(1)

Java内存模型的基础

并发编程模型的两个关键问题

线程之间如何通信及线程之间如何同步。,线程之间的通信机制有两种:共享内存和消息传递。同步是指程序中用于控制不同线程间操作发生相对顺序的机制。

Java内存模型的抽象结构

在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享 。局部变量(Local Variables),方 法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影 响。
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
在这里插入图片描述

从源代码到指令序列的重排序

重排序分3种类型:
在这里插入图片描述
上述的1属于编译器重排序,2和3属于处理器重排序。

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要 求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
在这里插入图片描述

happens-before简介

与程序员密切相关的happens-before规则如下。
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

重排序

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间 就存在数据依赖性。
在这里插入图片描述
这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程) 程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

double pi = 3.14; // A 
double r = 1.0; // B 
double area = pi * r * r; // C

在这里插入图片描述
C不能被重排序到A和B的前面。
在这里插入图片描述

程序顺序规则

根据happens-before的程序顺序规则,上面计算圆的面积的示例代码存在3个happens- before关系。
1)A happens-before B。
2)B happens-before C。
3)A happens-before C。

单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操 作重排序,可能会改变程序的执行结果。

顺序一致性

数据竞争与顺序一致性

当程序未正确同步时,就可能会存在数据竞争。Java内存模型规范对数据竞争的定义如下。
在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序
如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。

顺序一致性内存模型

顺序一致性内存模型有两大特性。
1)一个线程中的所有操作必须按照程序的顺序来执行。
2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。
在顺序一致性内 存模型中,每个操作都必须原子执行且立刻对所有线程可见。

假设这两个线程使用监视器锁来正确同步:
在这里插入图片描述
现在我们再假设这两个线程没有做同步:
在这里插入图片描述

同步程序的顺序一致性效果

在这里插入图片描述

未同步程序的执行特性

假设处理器A,B和C同时向总线发起总线事务,这时总线仲裁(Bus Arbitration) 会对竞争做出裁决,这里假设总线在仲裁后判定处理器A在竞争中获胜(总线仲裁会确保所有 处理器都能公平的访问内存)。此时处理器A继续它的总线事务,而其他两个处理器则要等待 处理器A的总线事务完成后才能再次执行内存访问。在这里插入图片描述

volatile的内存语义

volatile的特性

volatile变量自身具有下列特性。
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写 入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不 具有原子性。

volatile写-读建立的happens-before关系

在这里插入图片描述
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个 过程建立的happens-before关系可以分为3类:
1)根据程序次序规则,1 happens-before 2;3 happens-before 4。
2)根据volatile规则,2 happens-before 3。
3)根据happens-before的传递性规则,1 happens-before 4。

volatile写-读的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
在这里插入图片描述

volatile内存语义的实现

前文提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。表3-5是JMM针对编译器制定的volatile重排序规则表
在这里插入图片描述

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总 数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。

锁的内存语义

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

在这里插入图片描述
根据happens-before规则,这个 过程包含的happens-before关系可以分为3类。
1)根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens- before 6。
2)根据监视器锁规则,3 happens-before 4。
3)根据happens-before的传递性,2 happens-before 5。

锁的释放和获取的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
在这里插入图片描述

锁内存语义的实现

现在对公平锁和非公平锁的内存语义做个总结。
公平锁和非公平锁释放时,最后都要写一个volatile变量state。
公平锁获取时,首先会去读volatile变量。
非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile 写的内存语义。

从本文对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种 方式。
1)利用volatile变量的写-读所具有的内存语义。
2)利用CAS所附带的volatile读和volatile写的内存语义。

concurrent包的实现

由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现 在有了下面4种方式。
1)A线程写volatile变量,随后B线程读这个volatile变量。
2)A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
3)A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
4)A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量

如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式。
首先,声明共享变量为volatile。
然后,使用CAS的原子条件更新来实现线程之间的同步。
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

おすすめ

転載: blog.csdn.net/qq_42148307/article/details/121289559
おすすめ