并发编程模型的两个关键问题
java的并发采用的是共享内存模型,线程间通信是隐式进行的。
线程之间如何通信
通信是指线程之间以何种机制来交换信息,线程之间的通信机制有两种:
- 共享内存:线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。
- 消息传递:线程之间没有公共状态,通过互相发送消息进行显式通信。
线程之间如何同步
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
- 共享内存:同步是显式的,必须显式指定某个方法或代码段需要在线程之间互斥的执行。
- 消息传递:由于消息的接收必须在消息发送之后,因此同步是隐式的。
Java内存模型的抽象结构
Java内存模型(JMM)通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。
JMM定义了线程与主内存之间的抽象关系:
- 主内存,存储了线程之间的共享变量。
- 本地内存,存储了该线程用于读写共享变量的副本,是私有的。
从源码到指令序列的重排序
从Java源码到最终实际执行的指令序列,会分别经历下面三种重排序:
graph LR
源代码 --> 1.编译器优化重排序 --> 2.指令级并行重排序--> 3.内存系统重排序--> 最终执行的指令序列
其中:
- 1.编译器优化的重排序,是指编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
- 2.指令级并行的重排序,是指处理器采用指令级并行技术(ILP)将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。
- 3.内存系统的重排序,是指由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是乱序执行。
上述1属于编译器重排序,2、3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。
- 对于编译器重排序,JMM规则会禁止特定类型的编译器重排序。
- 对于处理器重排序,JMM规则会要求Java编译器生成指令序列时,插入特定的内存屏障(Memory Fence)指令来禁止特定类型的处理器重排序。
并发编程模型的分类
现代处理使用写缓冲区临时保存向内存写入的数据,以保证:
- 避免处理器停顿下来等待向内存写入数据;
- 以批处理的方式刷新写缓冲区、合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。
写缓冲区的问题:每个处理器上的写缓冲区对其它处理器不可见,因此处理器的读写操作执行顺序与实际刷新到内存的读写顺序不一定一致。
以如下代码为例,两个线程在不同的处理器并行执行,当执行顺序为A1->A2/B1->B2、内存刷新顺序为A2->A1/B2->B1时,可能执行结果为x=y=0:
public class ReOrdered {
private static class AB {
int a = 0;
int b = 0;
}
private static class XY {
int x = 0;
int y = 0;
}
public static void main(String[] args) throws InterruptedException {
ReOrdered.AB ab = new ReOrdered.AB();
ReOrdered.XY xy = new ReOrdered.XY();
Thread t1 = new Thread(() -> {
ab.a = 1;// A1
xy.x = ab.b;// A2
});
Thread t2 = new Thread(() -> {
ab.b = 2;// B1
xy.y = ab.a;// B2
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(ab.a);
System.out.println(ab.b);
System.out.println(xy.x);
System.out.println(xy.y);
}
}
内存屏障
常见的处理器都允许Store-Load重排序,都不允许对存在数据依赖的操作重排序.
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
jvm把内存屏障分为4类:
| 屏障类型 | 指令示例 | 说明 |
|---|---|---|
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令 |
| StoreStore Barriers | Store1;StoreStore;Store2 | 确保Store1数据对其它处理器可见,先于Store2及所有后续存储指令 |
| LoadStore Barriers | Load1;LoadStore;Store2 | 确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存 |
| StoreLoad Barriers | Store1;StoreLoad;Load2 | 确保Store1数据对其它处理器可见,先于Load2及所有后续装载指令。且使得该屏障之前的所有装载和存储指令完成后,才执行该屏障之后的内存访问指令 |
happens-before简介
JSR-133内存模型中,使用happens-before来描述操作间的内存可见性:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
注意happens-before只要求前一个操作的执行结果对后一个操作可见、且前一个操作按顺序排在第二个操作之前,并不意味着前一个操作必须在后一个操作之前执行。
规则如下:
- 程序顺序规则:一个线程中的每个操作,happens-before该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写操作happens-before任意后续对它的读操作。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。