什么是JMM
JMM定义了Java程序中各种变量(包括实例字段,静态字段和数组元素)的访问规则,以及线程和内存交互的规范。JMM的核心使命是解决对线程下由CPU缓存和指令重排引起的三大难题:原子性、可见性和有序性。
主内存和工作内存对的二分世界
JMM将内存抽象分为:主内存:是所有的线程共享的数据存储的区域,存放着所有的变量实例,而工作内存则是每一个线程私有的数据副本空间,每个线程只能操作自己本地内存中的变量,无法访问其他线程中的本地内存。这种设计源于现代计算机体系结构-CPU缓存和多级缓存的物理现实,抽过抽象使得Java程序能在不同的硬件架构上保持一致的并发语义。
线程需要操作变量的时候,必须经历以下交互流程:
- 读取:从主内存中拷贝变量到工作内存当中
- 修改:在工作内存中执行计算操作
- 回写:将修改的值刷新到主内存中
对线程环境中的内存挑战
现代处理器架构的三大特性直接催生了JMM的诞生
- CPU缓存分层:L1/L2/L3缓存的存在使得数据可能存在于多个副本中
- 指令重排:编译器和处理器为了优化性能可能改变指令的执行顺序
- 写缓冲区:处理器可能延迟写入操作以提高吞吐量
上述操作优化在单线程中完全透明,但是在多线程中会出现反直觉的行为。
为什么需要内存模型
如果没有JMM规范的多线程将会陷入混乱的状态,典型的灾难场景包括:
- 可见性失效:由于缓存为及时刷新,导致线程读取到过期数据
- 指令重排异常:看似有序的代码在运行时被重新排序
- long/double:64位变量在32位系统上被拆分为非原子性操作
并发编程的三个重要特性
原子性,可见性,有序性
原子性
一次操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么都不执行。
在Java中,可以借助synchronized、各种Lock以及各种原子类实现原子性。
可见性
当一个线程对共享变量进行了修改,另外的各种线程都可以立即可以看到修改后的最新值。
在Java中,可以借助synchronized、volatile以及各种Lock实现可见性。
如果我们将变量声明为volatile,这就表示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
有序性
由于指令重排序问题,代码的执行顺序未必就是编写的代码时候的顺序。
重排序:
重排序可以保证串行语义一致,但是没有义务保证多线程之间的语义也一致,所有在多线程下,指令重排序可能会导致一些问题。
在Java中,volatile关键字可以禁止指令进行重排序优化。