Java内存模型(一)
1. JMM基础——线程间的通信
处理并发编程,首先要处理的问题就是线程间如何通信并且如何做到同步,java的并发实现采用的是共享内存模型,线程之间的通信是隐式进行、对开发者完全透明的。
Java内存模型(JMM)控制线程之间的通信,决定一个线程对共享变量的写入什么时候对另一个线程可见。
JMM内存模型结构如下:
可以看到,如果线程A与线程B通信的话,需要经历两步:
1)线程A把更新的共享变量刷回主内存
2)线程B去主内存读取线程A更新过的变量
2. 重排序
2.1 JMM重排序规则
重排序指编译器和处理器为了优化程序性能而对指令序列重新排序的一种手段。
1)编译器:禁止特定类型的编译器重排序
2)处理器:插入内存屏障
从源码到最终执行的指令序列示意图如下:
处理器对内存的读写顺序不一定与内存中实际的读写顺序一致,因为有写缓冲区的存在,写缓冲区仅对其所在的处理器可见。
- 数据依赖性
在两个操作中,如果其中有一个操作为写操作,那么这两个操作具有数据依赖性,因为只要更改两个操作的执行顺序,程序的执行结果就会改变,为了保证执行结果的正确性,编译器不会的对具有数据依赖性的操作重排序。
如果重排序对程序的执行结果没有影响,JMM就会认为这种重排序是合法的。
2.2 内存屏障
在多线程环境里需要使用某种技术来使程序结果尽快可见。先假定一个事实:一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致。这个使内存数据对CPU核可见的技术被称为内存屏障或内存栅栏。
内存屏障类型:
- LoadLoad 屏障
序列:Load1,Loadload,Load2
确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。
- StoreStore 屏障
序列:Store1,StoreStore,Store2
确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。
- LoadStore 屏障
序列: Load1,LoadStore,Store2
确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。
- StoreLoad 屏障(全能型)
序列: Store1,StoreLoad,Load2
确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。
2.3 happens-before原则
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作间必须存在happens-before原则。
PS:happens-before原则的定义很微妙,仅仅要求前一个操作对后一个操作可见且前一个操作按顺序排在后一个操作之前,但并不意味前一个操作会在后一个操作之前执行。
happens-before原则:
- 程序顺序规则:一个线程中的每个操作,h-b与改线程的任意后续操作
- 监视器锁规则:对一个锁的解锁,h-b于对这个锁的加锁
- voliate变量规则:对一个voliate域的写,h-b于任意对这个voliate域的读
- 传递性:A h-b B,B h-b C,那么A h-b C.
2.4 重排序对多线程的影响
class Example{
int a = 0;
bool flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(falg){ //3
int i = a * a; //4
}
}
}
假设有两个线程A和B,A先执行writer()方法,随后B执行reader()方法,线程B在执行操作4时,能否看到线程A操作2的写入呢?答案是不一定。因为操作1和操作2没有数据依赖关系,所以可以对这两个操作重排序,执行时序图如下:
由时序图可以看出,多线程的语义被破坏。
操作3和操作4同样没有数据依赖关系,但是具有控制依赖关系,下面是对操作3和操作4重排序的时序图:
可以看出,提前猜测实质上是对操作3和操作4做了重排序,同样破坏了多线程的语义。