我们在这里简单复习一下操作系统中的缓存
操作系统中的缓存情况为上图,操作系统将内存,缓存分为多个大小相等的块。然后根据缓存的数目依次指定内存块所对应的缓存块,在使用时,直接访问缓存,未命中则更新。
但是操作系统的缓存有一个很关键的隐含信息:每个内存块只有一个缓存,一个缓存对应了多个内存。
但是在Java内存模型中,情况却似乎反了过来,如下图
Java内存模型
在Java中,
- 实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享(也就是共享变量
- 局部变量,方法定义参数,异常处理参数不会在线程之间共享(这些在语法上被限制了不能共享)
这就导致了线程可见性的问题,若两个线程希望通信:
在计算机的空间内,进程或者线程通信(只要两个线程有牵扯,都可以称为通信)只有两种方式
- 把信息放在一个公开的场合,等目标去读(共享内存
- 直接上门把信息送到(消息传递
但是对于JMM的模型,通信的实质还是通过共享变量的交互,
既然通过共享变量就注定要走这几个流程:
- 线程A,B读取共享变量到自己的缓存
- A做了操作,更新了缓存
- 缓存写回到主内存
- B更新缓存,得到信息
指令重排序
在操作系统、计算机组成等等中都有提到指令重排序。
假设当前有这样一个任务:
- 读取变量1,
- 计算变量1*500,
- 读取变量2,
- 计算变量1+变量2
在程序中,为了最大程度利用计算机的资源,会对指令进行重排序,保证各个设备是一直在工作的,或短时间内完成工作。
可能的执行顺序:1324
指令重排序的理念从计算机的底层到编译器都有应用到
重排序分3种:
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
但是在多线程的操作中,指令重排序意味着内存可见性的问题。
为了保证缓存一致性,内存可见性
- JMM的编译器会禁用一些情况的重排序
- JMM通过插入内存屏障来保证禁止特定类型的处理器重排序。
这里出现了一个新词语:内存屏障
什么是内存屏障?
内存屏障主要为读写屏障,是为了确保两步操作的先后(读读、写写、写读、读写)
而指令重排序的原则来源于happens-before
与程序员密切相关的happens-before规则如下(很重要)。
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。(对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。即可见性)
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
总结为四点:单线程不变,锁,volatile,传递
一个happens-before规则对应于一个或多个编译器和处理器重排序规则。Java程序员只需要熟悉该原则,无需理解下层实现。