1 Java内存模型(JMM)
- Java线程间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见
- JMM是一个抽象的概念,并非真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器的优化
- JMM定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
- 每个线程都有一个私有的本地内存,本地内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
- 同时JVM通过JMM来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
- 重要声明: JMM所描述的主内存、工作内存与Java内存区域的堆栈不是一回事,更准确是主内存就是内存条,为了提高性能,JVM可能会让工作内存优先存储在寄存器和高速缓存中,程序运行时主要访问读写的也是工作内存
2 JMM的核心原则
- JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
- 多线程并发的法宝:外互斥、内可见
2.1 原子性
- 原子性是指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
2.2 可见性
- 可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更
- Java中普通的共享变量不保证可见性,因为其的修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读"
- 缓存优化或者硬件优化或指令重排以及编辑器的优化都可能导致一个线程修改不会立即被其他线程察觉
- Java提供volatile保证可见性:写操作立即刷新到主内存,读操作直接从主内存读取
- Java同时还可以通过加锁的同步性间接保证可见性:synchronized和Lock能保证同一时刻只有一个线程获取锁并执行同步代码,并在释放锁之后将变量的修改刷新到主内存中
2.3 有序性
- 对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行
- 但为了提供性能,编译器和处理器通常会对指令序列进行重新排序
- 指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读"
3 JMM的抽象结构
4 JMM的影响范围
- 在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程间共享
- 局部变量、方法定义参数和异常处理器参数不在线程间共享,即不会有可见性问题也不受JMM影响
5 JMM的内存可见性保证
- 单线程程序: 不会出现内存可见性问题,不管重排序与否结果也会最终一致
- 正确同步的多线程程序: 将顺序一致性,JMM通过限制重排序来为程序提供内存可见性保证
- 未同步/未正确同步的多线程程序: JMM提供最小安全性保障-线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值
6 JMM的隐患
6.1 写缓冲区(工作内存)
- 处理器使用写缓冲区临时保存向内存写入的数据
- 写缓冲区可以保证指令流水线持续运行,避免由于处理器停顿等待向内存写入数据产生的延迟
- 写缓冲区还可以通过批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用
6.2 写缓冲区的隐患
- 虽然写缓冲区好处多多,但问题在于写缓冲区仅对其所在的处理器所见,即是处理器私有工作内存
- 为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序
- 因此,在JMM会存在缓存一致性问题和指令重排序的问题