文章目录
- 1,线程间的同步和通信
- 1.1, 共享内存并发模型 (Shared Memory Model)
- 线程通信机制
- 线程同步机制
- 特点
- 1.2, 消息传递并发模型 (Message Passing Model)
- 线程通信机制
- 线程同步机制
- 特点
- 适用场景对比
- 2,Java内存模型JMM
- 2.0,Java内存模型的基础
- (1)内存屏障
- (2)happens before
- 2.1,内存可见性
- (1)为什么会出现内存可见性问题?
- (2)内存可见性的发生过程
- (3)JMM如何保证内存可见性
- 2.2,JMM与重排序
- (1)指令重排序的类型
- (2)JMM如何限制重排序
- 2.3顺序一致性模型
- (1)核心定义
- (2)同步程序的顺序一致性效果
- 根据 JMM 的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取。
- Java 运行时内存区域和JMM
- Java 运行时内存区域描述的是在 JVM 运行时,如何将内存划分为不同的区域,并且每个区域的功能和工作机制。
- Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内存与工作内存之间安全地执行操作。它涵盖的主题包括变量的可见性、指令重排、原子操作等,旨在解决由于多线程并发编程带来的一些问题。(可见性,有序性,原子性)
- 指令重排是为了提高 CPU 性能,但是可能会导致一些问题,比如多线程环境下的内存可见性问题。
1,线程间的同步和通信
并发编程的线程之间存在两个问题:
-
线程间如何通信?即:线程之间以何种机制来交换信息
-
线程间如何同步?即:线程以何种机制来控制不同线程间发生的相对顺序
有两种并发模型可以解决这两个问题:
- 消息传递并发模型
- 共享内存并发模型
1.1, 共享内存并发模型 (Shared Memory Model)
Java主要采用这种模型
线程通信机制
- 通过共享内存进行通信
- 线程之间共享程序的公共状态(变量、对象等)
- 线程通过读写共享内存中的变量来隐式通信
线程同步机制
- 使用显式同步原语控制执行顺序
- 主要同步手段:
- 锁(synchronized, Lock)
- volatile变量
- 原子变量(AtomicInteger等)
- 内存屏障
特点
- 通信是隐式的(通过内存访问)
- 需要程序员显式控制同步
- 容易出现竞态条件、死锁等问题
1.2, 消息传递并发模型 (Message Passing Model)
如Go语言的channel、Actor模型
线程通信机制
- 通过发送和接收消息进行显式通信
- 线程/进程间没有共享状态
- 消息通道是唯一的通信媒介
线程同步机制
- 通信本身就是同步的(发送和接收操作)
- 常见实现方式:
- 同步消息传递(发送者阻塞直到消息被接收)
- 异步消息传递+消息队列
- CSP(Communicating Sequential Processes)模型
特点
- 通信是显式的(明确的send/receive操作)
- 同步内建于通信机制中
- 避免了共享内存带来的许多问题
适用场景对比
场景 | 推荐模型 | 原因 |
---|---|---|
分布式系统 | 消息传递 | 天然适合网络通信 |
单机高并发 | 共享内存 | 性能更高 |
简单并发任务 | 消息传递 | 更易实现和维护 |
复杂数据共享 | 共享内存 | 更高效的数据访问 |
容错系统 | 消息传递 | 更好的隔离性和恢复能力 |
实时系统 | 共享内存 | 更低延迟 |
2,Java内存模型JMM
Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中定义的一种内存访问规范,它是一种抽象概念,包含缓存、写缓冲区、寄存器等。它规定了多线程环境下如何正确地访问共享变量,以及线程之间如何通过内存进行通信。即解决上述的“线程间如何通信”和“线程间如何同步两个问题”。保证多线程环境下的可见性、有序性和原子性。
JMM解决的三大问题
问题类型 | 描述 | JMM解决方案 |
---|---|---|
可见性 | 一个线程修改共享变量后其他线程立即可见 | volatile、synchronized、final |
有序性 | 指令执行顺序与代码顺序一致 | happens-before、内存屏障 |
原子性 | 操作不可中断 | synchronized、原子类 |
2.0,Java内存模型的基础
(1)内存屏障
屏障类型 | 作用 |
---|---|
LoadLoad | 禁止 Load1 和 Load2 重排序 |
StoreStore | 禁止 Store1 和 Store2 重排序 |
LoadStore | 禁止 Load 和后续 Store 重排序 |
StoreLoad | 禁止 Store 和后续 Load 重排序 |
eg:LoadLoad屏障
确保 Load1
先于 Load2
执行,防止读操作重排序。
StoreLoad 屏障(全能屏障)
- 作用:
- 禁止
Store
和后续Load
重排序。 - 强制刷新所有写操作到主内存,并 使其他 CPU 缓存失效。
- 禁止
- 开销最大,但能保证最强的内存一致性。‘
(2)happens before
Happens-Before 是 Java 内存模型(JMM)的核心概念,它定义了多线程环境下操作之间的可见性保证和执行顺序约束,使开发者能够在不深入理解底层内存屏障的情况下编写正确的并发程序。
Happens-Before 描述的是两个操作之间的偏序关系:
- 如果操作 A happens-before 操作 B,那么:
- A 的执行结果对 B 可见
- A 的代码顺序在 B 之前
📌 注意:Happens-Before 并不一定代表时间上的先后,而是可见性保证!
happens-before的六大规则
- 程序顺序规则:同一线程中的操作,按照代码顺序 happens-before。
- 监视器锁规则:解锁(unlock) happens-before 后续的加锁(lock)。
- volatile 变量规则:volatile 写 happens-before 后续的 volatile 读。
- 线程启动规则:
Thread.start()
happens-before 该线程的所有操作。 - 线程终止规则:线程的所有操作 happens-before 其他线程检测到它终止(如
t.join()
)。 - 传递性规则:如果 A happens-before B,且 B happens-before C,则 A happens-before C。
2.1,内存可见性
内存可见性(Memory Visibility)是多线程编程中的一个核心概念,指的是当一个线程修改了共享变量的值后,其他线程能否立即看到这个修改。如果修改后的值不能及时被其他线程观察到,就会导致内存可见性问题,从而引发程序逻辑错误。
什么是共享变量
共享变量是指在多线程环境下可以被多个线程共同访问和修改的变量。
对于每一个线程来说,栈都是私有的,而堆是共有的。也就是说,在栈中的变量(局部变量、方法定义的参数、异常处理的参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,一般称之为共享变量。所以,内存可见性针对的是堆中的共享变量。
(1)为什么会出现内存可见性问题?
现代计算机和 JVM 为了提高性能,会采用以下优化策略,导致内存可见性问题:
(1) CPU 缓存架构
- CPU 不会直接读写主内存(RAM),而是通过**多级缓存(L1/L2/L3 Cache)**来提高访问速度。
- 每个 CPU 核心有自己的缓存,线程运行时可能只更新自己的缓存,而不会立即同步到主内存。
- 因此,一个线程的修改可能对其他线程不可见。
(2) 指令重排序(Reordering)
-
编译器优化:JIT 编译器可能会调整指令顺序以提高性能。
-
CPU 乱序执行:CPU 可能会改变指令的执行顺序(只要不影响单线程语义)。
-
这可能导致线程 A 的修改操作被延迟或乱序执行,导致线程 B 看到的数据不一致。
public class ReorderingProblem {
private static int x = 0;
private static int y = 0;
private static boolean ready = false;public static void main(String[] args) { Thread writer = new Thread(() -> { x = 1; y = 2; ready = true; // 可能被重排序到前面 }); Thread reader = new Thread(() -> { while (!ready); // 等待ready=true System.out.println("x=" + x + ", y=" + y); // 可能输出x=0, y=2 }); writer.start(); reader.start(); }
}
由于指令重排序问题,可能会ready = true
可能先于 x = 1
执行,导致 reader
线程看到 x=0
,但 y=2
。
(3) 工作内存(Working Memory)抽象
- JMM(Java 内存模型)规定,每个线程有自己的工作内存(可以理解为 CPU 缓存 + 寄存器 + 写缓冲区)。
- 线程操作共享变量时,先在工作内存中修改,再同步回主内存,这可能导致其他线程看不到最新值。
(2)内存可见性的发生过程
从图中可以看出:
- 所有的共享变量都存在主存中。
- 每个线程都保存了一份该线程使用到的共享变量的副本。
- 如果线程 A 与线程 B 之间要通信的话,必须经历下面 2 个步骤:
- 线程 A 将本地内存 A 中更新过的共享变量刷新到主存中去。
- 线程 B 到主存中去读取线程 A 之前已经更新过的共享变量。
所以,线程 A 无法直接访问线程 B 的工作内存,线程间通信必须经过主存。
注意,根据 JMM 的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取。
所以线程 B 并不是直接去主存中读取共享变量的值,而是先在本地内存 B 中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存 B 去主存中读取这个共享变量的新值,并拷贝到本地内存 B 中,最后线程 B 再读取本地内存 B 中的新值。
(3)JMM如何保证内存可见性
Java内存模型(JMM)的核心作用之一就是解决"如何知道共享变量被其他线程更新了"这个问题。
JMM 通过控制主存与每个线程的本地内存之间的交互,来提供内存可见性保证。
Java 中的 volatile 关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized 关键字不仅保证可见性,同时也保证了原子性(互斥性)。
在更底层,JMM 通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员更方便地理解,设计者提出了 happens-before 的概念,它更加简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则,以及这些规则的具体实现方法。
2.2,JMM与重排序
Java内存模型(JMM)的一个重要方面就是管理指令重排序(Reordering),它定义了在多线程环境下哪些重排序是被允许的,哪些是被禁止的。理解这一点对编写正确的并发程序至关重要。
(1)指令重排序的类型
- 编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
- 指令级并行的重排序
现代处理器采用指令级并行技术(ILP)来将多条指令重叠执行
- 内存系统的重排序
由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是在乱序执行
(2)JMM如何限制重排序
JMM通过以下几种机制来限制重排序:
- happens-before规则
定义了一系列天然的happens-before关系,在这些关系下禁止重排序:
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 线程启动/终止规则
- 传递性规则
- 内存屏障(Memory Barrier)
JMM在关键位置插入内存屏障指令来禁止特定类型的重排序:
屏障类型 | 作用 |
---|---|
LoadLoad | 禁止Load1与Load2重排序 |
StoreStore | 禁止Store1与Store2重排序 |
LoadStore | 禁止Load与后续Store重排序 |
StoreLoad | 全能屏障,禁止Store与后续Load重排序(开销最大) |
- 特殊关键字语义
- volatile:禁止与相邻指令重排序
- final:保证正确构造后的对象对所有线程可见
- synchronized:进入/退出时隐含内存屏障
2.3顺序一致性模型
(1)核心定义
顺序一致性模型必须满足两个基本条件:
- 程序顺序保留:每个线程内部的操作必须按照该线程的程序代码顺序执行。(不允许重排序)
- 全局内存顺序:所有线程看到的整个系统的操作执行顺序必须一致
顺序一致性模型虽然理论上完美,但硬件上难以实现,但Java等语言可以提供近似保证。
(2)同步程序的顺序一致性效果
在并发编程中,通过同步机制可以使程序表现出顺序一致性的内存效果,即使底层硬件和编译器可能进行各种优化。