1.背景
JMM(Java Memory Model)
的提出,主要基于以下的几种原因:
- 不同操作系统平台的内存模型不同,而
Java
又想做到Write Once Run Everywhere
(即跨平台),那么必须要自己提供一套内存模型以屏蔽不同操作系统在内存模型方面的差异。 - 由于除了编译器层面可以进行指令重排外,处理器层面也可以,尽管指令重排在一定程度上能够提升程序运行的效率,但这仅限于单线程环境下,一旦处在多线程环境,这种指令重排带来的更多的是并发性问题(比如多级缓存与内存中的数据不一致),而Java又是多线程语言,那么就会存在多线程编程。为了保证开发者编写的并发程序能够按照预期安全地执行,Java必须提出一套并发编程相关的规范(比如
happens-before原则
)来解决多线程环境下的可能存在的并发性问题。
2.内容
2.1 内存规范
2.1.1 内存抽象
- 本地/工作内存:每个线程私有且独占,不可跨线程访问的物理内存区域。存放了共享变量的副本。
- 主内存:线程间共享的逻辑内存区域(并不真实存在)。存放所有线程创建的实例对象,包括:成员变量、局部变量、类信息、常量、静态变量等。线程间通信必须借助主内存来进行。
2.1.2 内存操作
下面的8
种操作主要用于主内存与工作内存的数据交互:
- 锁定(lock):将主内存中的一个
共享变量
标记为线程独享变量
。 - 读取(read):将主内存中的一个
共享变量
传递到线程的工作内存
中。 - 载入(load):将从主存(通过
read
)读到的共享变量
装载进工作内存
中的相应的共享变量副本
中。 - 使用(use):将
共享变量的副本
传递给执行引擎
,供其使用。(每次需要用到该变量时,都会执行该指令) - 赋值(assign):将从
执行引擎接收到的结果值
赋值给共享内存的副本
。(每次的赋值操作均会调用该指令) - 存储(store):将
共享变量的副本
传递回主内存
中。 - 写入(write):将
接收到的共享变量的副本
(新修改的共享变量)写回主内存
中的相应的共享变量
中。 - 解锁(unlock):将主内存中的一个
共享变量
的线程独享变量
的状态标记取消。
注:
JMM
规范中并没有明确说明read
和store
将读到的共享变量(副本)存放在工作内存/主内存的什么地方,但是肯定没有存发在共享变量(副本)对应的槽位上(类似于找一个地方暂存一下数据的意思),因为它需要load
和write
指令正式存放到相应的变量槽位上去。
要求:
- 同一时间,只能有一个线程持有某一共享变量的锁(通过
lock
指令获取),获得锁的线程可以多次加锁(同一个),但是释放锁时需要释放同等次数(类似于ReentrantLock
)。 使用
共享变量之前,必须先读取
并载入
,即不允许在工作内存中读取一个凭空诞生(主内存中没有)的变量。- 不允许写回一个没有
赋值
的共享变量副本
。(这样做你觉得有意义?😅) - …(此处省略很多字)
2.2 并发规范
2.2.1 happends-before原则
一句话概括就是:前一个操作的结果对后一个操作是可见的,无论这两个操作是否处在同一个线程里。
8项原则:
- 程序顺序原则:在同一个线程内的代码中,
写在前面的代码
happens-before写在后面的代码
。 - 锁规则:
加锁
happens-before释放锁
。 - volatile规则:对于一个
volatile变量的写操作
happens-beforevolatile变量的读操作
。(该原则并不是说你只能先写后读一个volatile
变量,而是说,前一个操作如果是写入volatile
变量的操作,那么该volatile
变量的新值对于后续的volatile
读操作一定是可见的) - start规则:
线程的start()动作
happens-before线程内的每个动作
。 - join规则:
线程内的所有动作
happens-before线程的join()动作
。 - interrupt规则:
对线程调用interrupt()
happens-before线程自己检测到中断事件的发生
。 - finalize规则:
一个对象的构造函数的执行、结束和返回
happens-before这个对象finalize()方法的开始
。 - 传递性:
A
happens-beforeB
,B
happens-beforeC
,那么就有A
happens-beforeC
。
3.注意事项
3.1 JMM
与JVM内存结构
的区别
- JMM:与
Java并发编程
相关,抽象了线程与主内存的关系,规定了并发相关的原则和规范,以简化多线程编程,增强程序的可移植性。 - JVM内存结构:与
JVM运行时区域
相关,定义了JVM
如何分区存储不同类型的数据。
3.2 happens-before
与JMM
的关系
JMM
通过自行禁用处理器的若干重排序规则或者直接禁用某种类型的处理器来实现它向程序猿发出的happens-before
几点保证,因为处理器的重排序规则在并发环境下可能会产生未知差错从而使得happens-before
失去保证,所以必须要根据实际的运行情况进行适当的禁用。
4.补充
4.1 并发编程的三大重要特性
- 原子性:一次操作或者多个操作要么全都执行,要么全都不执行。(这里不包括执行失败后回滚的情况,即
并发编程的原子性 ≠ 事务性的原子性
) - 可见性:当一个线程对某一个共享变量进行了修改,那么其他线程将能够立即看到修改后的新值。
- 有序性:因为编译器和处理器对代码进行指令重排序的缘故,你所编写的代码的顺序不一定就是代码实际的执行顺序。
参考文档
JMM(Java 内存模型)详解