Java内存模型(JMM)抽象了线程和主内存之间的关系,比如:线程之间的共享变量必须存储在主内存中。Java内存模型下,线程可以把变量保存到本地内存中,而不是直接在主内存中进行读写。这就可能造成一个线程在主内存中修改了一个变量的值,而另外一个线程还继续使用它在本地内存中的变量值的拷贝,造成数据的不一致。这和CPU缓存(上篇文章缓存一致性)模型非常相似。
Java 内存模型是一种规范,规范如下:
1. 所有的变量都存储在主内存(Main Memory)中。
2. 每个线程都有一个本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
3. 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
4. 不同的线程之间无法直接访问对方本地内存中的变量。
主内存和本地内存(工作内存)的理解:
主内存:所有线程创建的实例对象都存放在主内存中(包括成员变量和局部变量)
本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本,本地内存是JMM抽象出来的一个概念,存储了主内存中的共享变量副本。
Java 内存模型的抽象示意图如下:
我们可以看到单个线程操作同一个变量不会出现问题。
当多线程情况下操作同一个共享变量就会出现线程安全问题。比如线程1操作“共享变量1”做10次自增,线程2操作“共享变量1”做10次自增,理论上“共享变量1”应该是+20。但实际不一定是的,就出现了线程安全问题。
关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作:
锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行:
不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。
对happens-before的理解
happens-before规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结。JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。
前面说到JMM指定了一组排序规则,来保证线程之间的可见性。这一组规则即被称为 happens-before。
happens-before设计原则:
happens-before原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。happens-before 原则的设计思想其实非常简单:
1. 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
2. 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。JSR-133对happens-before原则的定义:
1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。
happens-before常见规则如下:
happens-before的规则有8条,重点讲解下面列举的6条:
1.单线程规则。 一个线程中的每个动作都happens-before该线程中后续的每个动作。
2.监视器锁定规则。 监视器的解锁动作happens-before后续对这个监视器的锁定动作。
3.volatile变量规则。 对volatile字段的写入动作happens-before后续对这个字段的读取动作。
4.线程start规则。 线程start()方法的执行happens-before一个启动线程内的任意动作。
5.线程join规则。 一个线程内的所有动作happens-before任意其他线程在该线程join()成功返回之前。
6.传递规则。 如果A happens-before B,且B happens-beforeC,那么A happens-before C。
如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。
Java内存模型总结:
1. Java 是最早尝试提供内存模型的语言,其主要目的是为了简化多线程编程。
2. CPU 可以通过制定缓存一致协议来解决内存缓存不一致性问题。
3. 为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。
4. JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。