介绍

Java内存模型(Java Memery Model)用来屏蔽掉各种硬件和操作系统的内存访问差异。以至于让Java在各中平台下都能达到一致的内存访问效果。

简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障.

从硬件角度来看。因为处理器的运算速度很快,比如做一个递增操作,就需要从内存中拿值,操作后再放回内存。这样的I/O是无法避免的,但这I/O速度和处理器的运算速度就不是一个数量级,所以为了解决这个问题,就对每个处理器加一个高速缓存(Cache)来作为处理器与内存之间的缓冲。把需要使用的数据复制到缓存中,运算完成之后在从缓存同步到内存。这样处理器就不用等待缓慢的内存读写了。

20210416212138

Java内存模型图

20210416212236

主存与工作内存的一些交互指令

操作 作用对象 解释
lock 主内存 把一个变量标识为一条线程独占的状态
unlock 主内存 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定
read 主内存 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用
load 工作内存 把 read 操作从主内存中得到的变量值放入工作内存中
use 工作内存 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作
assign 工作内存 把一个从执行引擎接收到的值赋接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store 工作内存 把工作内存中的一个变量的值传送到主内存中,以便 write 操作
write 工作内存 把 store 操作从工作内存中得到的变量的值放入主内存的变量中

例子:
20210416212426

对于volatile型变量的特殊规则

关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。保存了线程可见性与防止指令重排。

一个变量被定义为volatile的特性

1.保证此变量对所有线程的可见性。但是对变量的操作如果不是原子操作,那么并发情况下不安全。如果不满足以下条件就,需要加锁保证并发安全。

  • 运算结果并不依赖变量当前值
  • 能够确保只有单一的线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束

2.禁止指令重排序优化。通过插入内存屏障保证一致性。

对于long和double型变量的特殊规则

Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。

原子性、可见性与有序性

原子性(Atomicity)
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。

可见性(Visibility)
是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。同步块的可见性是由“对一个变量执行unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。

有序性(Ordering)
如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻只允许一条线程对其进行 lock 操作”这条规则获得,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

先行发生原则

也就是 happens-before 原则。这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系。

天然的先行发生关系
|规则| 解释|
|:—-:|:—-:|:—-:|
|程序次序规则| 在一个线程内,代码按照书写的控制流顺序执行|
|管程锁定规则| 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作|
|volatile 变量规则| volatile 变量的写操作先行发生于后面对这个变量的读操作|
|线程启动规则| Thread 对象的 start() 方法先行发生于此线程的每一个动作|
|线程终止规则| 线程中所有的操作都先行发生于对此线程的终止检测(通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值检测)|
|线程中断规则| 对线程 interrupt() 方法调用优先发生于被中断线程的代码检测到中断事件的发生 (通过 Thread.interrupted() 方法检测)|
|对象终结规则| 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始|
|传递性| 如果操作 A 先于 操作 B 发生,操作 B 先于 操作 C 发生,那么操作 A 先于 操作 C|

参考

Java内存模型JMM

评论