前言
本篇主要讲解Java内存模型的基本概念,在讲解之前我们先了解一下硬件的内存模型。
由于CPU处理速度很快,而内存远远跟不上CPU。为了提高性能,在CPU与内存之间加了一层高速缓存(Cache)来作为内存与CPU之间的缓冲:将运算需要用到的数据复制到缓存,加速运算,当运算完后再将缓存中的数据同步到内存。
高速缓存解决了CPU运算快而内存读写慢导致的导致的处理效率问题,但是又引起了一个新的问题:缓存一致性(Cache Coherence)。在多CPU计算机中,每个CPU都有自己的高速缓存,而多个CPU共享同一个主存,这就需要有一种协议保障数据的一致性。一致性协议包括:MSI,MESI,MOSI,Synapse,Firefly,Dragon Protocol。
概述
内存模型即是在特定的协议下,对特定的内存或高速缓存进行读写访问的过程的抽象,不同架构的物理机拥有不一样的内存模型。C/C++语言中直接使用物理硬件和操作系统内存模型,导致不同平台下并发访问出错,而Java内存模型屏蔽了各种硬件和操作系统的内存访问差异。
Java内存模型规定了所有变量都存储在主内存中,每个线程有自己的工作内存,线程对变量的所有读写操作都必须在工作内存中进行,不同的线程之间也无法访问对方工作内存中的变量,线程之间变量值的传递均需要通过主内存来完成。工作内存中保存了该线程需要用到的变量为主内存中变量的副本。这里变量包括实例域、静态域和数组元素,但不包括局部变量、方法参数和异常处理参数,因为他们不在线程之间共享,所以不受内存模型影响。
原子性、有序性、可见性
JMM定义了工作内存和主内存之间变量访问的细节,通过保障原子性、有序性、可见性实现线程安全。
- 原子性:一个操作是不可中断的,要么全部执行成功要么全部执行失败。即使在多个线程一起执行时,一旦操作开始,就不能被其他线程干扰。java内存模型中定义了8中操作都是原子的,包括:lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(操作)。其中,read、load、use、assign、store、write,可以大致认为基本数据类型的访问读写具备原子性,如果需要大范围的原子操作可以使用lock、unlock操作,但是并没有给程序员提供这两个操作,但jvm提供了monitorenter和monitorexit这连个指令开放给我们使用,也就是我们代码中的synchronized关键字。
- 有序性:程序按照顺序执行。由于存在重排序(下文会提到),在多线程的时候会导致错误的结果发生。Java有两个关键字来保证有序性:volatile与synchronized。volatile包含禁止重排序的语义。synchronized语义要求线程在访问共享变量时只能“串行”执行。
- 可见性:当多个线程同时访问一个共享变量时,其中某个线程更改了该变量,其他线程可以立即看到。valatile的特殊规则保证了新值能立即同步到主内存,以及每次是使用前都能从主内存刷新。synchronized在执行unlock之前,需要将变量同步至主内存。final常量无需同步,就能被其它线程正确访问。
重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。Java从源代码到最终实际执行的指令序列,会经历以下三种重排序:
这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器优化重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序。
内存屏障类型:
处理器的重排序规则:
happens-before原则
happens-before中确定了8条规则,如果如果两个操作之间的关系可以从下列规则推导出来说明两个操作是有序的:
1.程序次序原则:一个线程内,按照程序代码顺序,书写在前面的操作先发生于书写在后面的操作。
2.volatile 规则:volatile 变量的写,先发生于读,这保证了 volatile 变量的可见性。
3.锁规则:解锁(unlock) 必然发生在随后的加锁(lock)前。
4.传递性:A先于B,B先于C,那么A必然先于C。
5.线程的 start 方法先于他的每一个动作。
6.线程的所有操作先于线程的终结。
7.线程的中断(interrupt())先于被中断的代码。
8.对象的构造函数,结束先于 finalize 方法。