什么是JMM
JMM(Java Memory Model) 是Java虚拟机规范中定义的并发编程相关的一组规范。除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,来试图屏蔽不同硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果。
为什么要遵守这些并发相关的原则和规范呢? 这是因为并发编程下,像CPU多级缓存
和指令重排序
这类设计可能会导致程序运行出现一些问题。
为了实现这个目标,JMM规定了并发编程的三个关键特性,这三个特性在多线程编程中非常重要,因为它们保证了多个线程之间对共享数据的正确操作。
原子性:多个线程对共享变量的操作必须是原子性的,即每个操作都不能被其他线程中断或干扰。
在 Java 中,可以借助synchronized
、各种 Lock
以及各种原子类实现原子性。synchronized
和各种 Lock
可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS
(compare and swap) 操作(可能也会用到 volatile
或者final
关键字)来保证原子操作。
可见性:一个线程对共享变量的修改必须对其他线程可见,以确保所有线程都能看到最新的值。
在 Java 中,可以借助synchronized
、volatile
以及各种 Lock
实现可见性。
如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
有序性:即程序执行的顺序按照代码的先后顺序执行。
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
在 Java 中,可以通过happens-before
原则和volatile
关键字保证有序性。
线程和主内存之间的关系
JMM规定了线程和主内存之间的抽象关系。线程之间的共享变量存储在主内存中,每个线程都有自己的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
什么是主内存?什么是本地内存?
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量。
本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,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 规则?
JVM 会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,我们可以使用volatile
和synchronized
关键字来避免,但是如果Java内存模型中所有的有序性都仅仅靠volatile
和synchronized
来完成,那么有一些操作将变得十分繁琐。但是我们在写Java并发代码时并没有感觉到这一点,这是因为Java语言中有一个Happens-before
原则。这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则我们可以通过几条规则判断并发环境下两个操作之间是否可能存在冲突。
happens-before 常见规则
- 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变
- 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果(管程是一种通用的同步原语,synchronized就是管程的实现)
- volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见
- 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
- 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断
- 对象终结规则:就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法
- 传递规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C