目录
JMM的简介
Java线程之间的通信由Java内存模型控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见
JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存(堆)中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
从硬件内存结构的角度来说,本地内存并不存在,本地内存实际是包括了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
线程A与线程B之间如果要通信
-
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去
-
然后,线程B到主内存中去读取线程A之前更新过的共享变量。
如下图所示:
此图中,本地内存A和B有主内存中共享变量x的副本,假设在初始时,本地内存A、本地内存B和主内存中x都为0。现在要执行以下三个步骤:
-
线程A将x=0修改为x=1,并存放在自己的本地内存A中
-
然后线程A要向线程B发送消息,此时线程A就会将自己更新过x的值刷新到主内存中,此时主内存中的x值变成了1
-
线程B收到了A发送的消息,随后线程B就会到主内存中读取线程A更新后的x的值,此时线程B的本地内存的x值也变成了1
总结:
-
JMM主要通过控制主内存与每个线程的本地内存之间的交互,来保证线程之间共享变量的可见性。
-
线程A无法直接访问线程B的工作内存,线程之间的通信必须经过主存
-
线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取
指令重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序
重排序分为三种类型:
-
编译器优化的重排序
编译器在不改变单线程的语义的前提下,可以重新安排语句的执行顺序
-
指令级并行的重排序
现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的顺序
-
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
上图中,1属于编译器重排序,2和3属于处理器重排序,这些重排序都可能会导致多线程程序出现内存可见性问题
JMM的编译器重排序规则会禁止特定类型的编译器重排序
JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序
处理器重排序和内存屏障指令
对于处理器的写缓冲区特性的描述:
现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线的持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。
虽然写缓冲区有很多好处,但是每个处理器上的写缓冲区,仅仅是对它所在的处理器可见。这样的特性会对内存操作的执行顺序产生重要的影响:
- 处理器对内存的读写操作的执行顺序,不一定与内存实际发生的读写操作顺序一致
示例:
int a = 0
int b = 0
// 线程A
a = 1; //A1
x = b; //A2
// 线程B
b = 2; //B1
y = a; //B2
// 初始状态:a = b = 0;处理器允许执行后得到结果:x = y = 0
-
A2:线程A要操作共享变量a,因此先在主存中读取a(a=0);
-
B2:线程B要操作共享变量b,因此也先从主存中读取b(b=0);
-
A1:此时线程A将a设置为1,存储到缓冲区中,线程A的缓冲区中变为(a=1)
-
B1:同时线程B将b设置为2,存储到缓冲区中,线程B的缓冲区中变为(b=2)
-
A2:线程A中将b的值赋值给x,就会先去主存中读取共享变量b(b=0)
-
B2:线程B中将a的值赋值给y,也会先去主存中读取共享变量a(a=0)
-
A1:x=b=0,存储到缓冲区中
-
B1:y=a=0,存储到缓冲区中
-
A3、B3:最终将a,b,x,y的值写入主存,a=1;b=2;x=0;y=0,与实际想要得到的结果不符
此处处理器A和处理器B同时将共享变量a,b写入自己的缓冲区(操作为A1,B1),然后从内存中读取另一个共享变量(操作为A2,B2),最后将自己写缓存区保存的脏数据刷新到主内存中(操作为A3,B3)
从内存操作的实际发生来看:
处理器A开始执行A3,写操作A1才算真正执行了
虽然处理器A执行操作的顺序是:A1→A2;但是内存操作发生的顺序却是:A2→A1
即可以理解为,A1操作还没完成,A2就执行并完成了
这种我们视为处理器A的内存操作顺序被重排序了
这里的关键点是:由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。只要使用了写缓冲区都会允许对读写操作的重排序
下表是常见处理器的重排序类型的列表
Load-Load | Load-Store | Store-Store | Store-Load | 数据依赖 | |
---|---|---|---|---|---|
sparc-TSO1 | 不允许 | 不允许 | 不允许 | 允许 | 不允许 |
x862 | 不允许 | 不允许 | 不允许 | 允许 | 不允许 |
ia64 | 允许 | 允许 | 允许 | 允许 | 不允许 |
PowerPC3 | 允许 | 允许 | 允许 | 允许 | 不允许 |
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令,来禁止特定类型的处理器重排序
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载,在Load2以及所有后续装载指令的装载之前 |
StoreStore Barriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(强制刷新到内存),在Store2以及所有后续存储指令的存储之前 |
LoadStrore Barriers | Load1;LoadStore;Store2 | 确保Load1数据的装载,在Store2以及所有后续存储指令刷新到内存之前 |
StoreLoad Barriers |