JMM学习笔记
JMM:Java Memory Model 内存模型
在不同的操作系统下,内存访问逻辑有一定的差异,JMM就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。
作用:缓存一致性协议,用于定义数据读写的规则
JMM是一种抽象的概念,并不真实存在,它描述了一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和后成数组对象的元素)的访问方式
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存Main Memory中,每个线程都有一个私有的本地内存Local Memory

会出现一个问题:当一个线程修改了自己工作内存中的变量,对其他线程是不可见的,会导致线程不安全的问题。因此JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
解决共享对象可见性的问题:volatile
JMM定义的规则
内存交互操作
有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的
- lock锁定:作用于主内存的变量,把一个变量标识为线程独占状态
- unlock解锁:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read读取:作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作
- load载入:作用于工作内存的变量,把read操作从主内存中变量放入工作内存
- use使用:作用于工作内存的变量,把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign赋值:作用于工作内存的变量,把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store存储:作用于主内存的变量,把一个从工作内存中一个变量的值传送到主内存中,以便以后write的使用
- wirte写入:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令制定了如下的规则
- 不允许read和load,store和wirte操作之一单独出现
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化(load或assign)的变量。
- 一个变量同一时间只有一个线程能对其进行lock,且对于多次loack,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有lock,就不能进行unlock,也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock之前,必须把此变量同步回主内存
上述规则➕volatile的特殊规则可以判断哪里操作是线程安全,哪里操作是线程不安全
上述规则过于复杂,更多的时候,使用Java的happen-before规则进行分析
模型特征
原子性Atomicity
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么都不执行
eg
int k = 5; //原子操作
k++; //三个操作 读取k的值 k的值加1 计算后的值再赋值给变量k
如果应用场景需要一个更大范围的原子性保证,JMM还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,反应到Java代码中就是同步块——synchronized关键字
可见性visibility
指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
//线程A
int k = 0;
k = 5;
//线程B
int j = k;
/*
如果线程A先执行,线程B再执行,j的值是多少?
无法确定。
工作内存所更新的变量并不会立即同步到主内存,因此线程B从主内存中得到的变量k的值是不确定的,这就是可见性问题
*/
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值。
有序性ordering
一个线程的所有操作必须按照程序的顺序来执行
int k = 0;
int j = 1;
k = 5;//代码1
j = 6;//代码2
/*
JVM并不保证上面代码1和2的执行顺序,因为这两行代码没有数据依赖性,先执行那一行代码最终的结果都不会改变,因此,JVM可能会进行指令重排序
*/
int k = 1;
int j = k;
/*
因为代码2依赖代码1的执行结果,因为JVM不会对这两行代码进行指令重排序
*/
Java语言提供了volatile和synchronized关键字来保证线程之间操作的有序性。
volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
重排序
-
什么是重排序?
编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段
-
为什么要重排序?
为了提升性能
-
重排序会导致不正确的结果吗?
在单线程下不会改变执行结果,多线程下可能会改变
-
指令重排序的条件
在单线程环境下不能改变程序的运行结果
存在数据依赖关系的不允许重排序
无法通过Happens-before原则推出来的,才能进行指令的重排序
HAPPEN-BEFORE先行发生原则
先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则解决并发环境下两个操作之间是否可能存在冲突的所有问题。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,先行发生的操作A产生的影响能被操作B观察到
“天然的”先行发生关系
可以直接在编码中使用,无须任何同步器协助
- 程序次序规则Program Order Rule:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 管程锁定规则Monitor Lock Rule:一个unlock操作先行发生于后面对同一个锁的lock操作,这里强调必须是同一个锁
- volatile变量规则Volatile Variable Rule:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则Thread Start Rule:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则Thread Terminate Rule:线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行
- 线程中断规则Thread Interruption Rule:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生
- 对象终结规则Finalizer Rule:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
- 传递性Transitivity:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C
//以下操作在同一个线程执行
int i = 1;
int j = 2;
/*
两条赋值语句在同一个线程中,根据程序次序规则,int i = 1;的操作先行发生,但是int j = 2;的代码完全可能先被处理器执行,这并不影响先行发生原则的正确性。
RESULT:时间先后顺序与先行发生原则之间基本没有太大的关系,在衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准
关于volatile
volatile的特性
Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量定义为volatile之后,它将具备两种特性。
- 保证此变量对所有线程的可见性,即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
- 禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
volatile使用场景
-
状态标记量
使用volatile来修饰状态标记量,使得状态标记量对所有线程是实时可见的,从而保证所有线程都能实时获取到最新的状态标记量,进一步决定是否进行操作。例如常见的促销活动“秒杀”
-
双重检测机制实现单例
普通的双重检测机制在极端情况,由于指令重排序会出现问题,通过使用volatile来修饰instance,禁止指令重排序,从而可以正确的实现单例。
volatile总结
- 每个Java线程都有自己的工作内存,工作内存中的数据并不会实时刷新回主内存,因此在并发情况下,有可能线程A已经修改了成员变量k的值,但是线程B并不能读取到线程A修改后的值,这是因为线程A的工作内存还没有被刷新回主内存,导致线程B无法读取到最新的值。
- 在工作内存中,每次使用volatile修饰的变量前都必须先从主内存刷新最新的值,这保证了当前线程能看见其他线程对volatile修饰的变量所做的修改后的值。
- 在工作内存中,每次修改volatile修饰的变量后都必须立刻同步回主内存中,这保证了其他线程可以看到自己对volatile修饰的变量所做的修改。
- volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。
- volatile保证可见性,不保证原子性,部分保证有序性(仅保证被volatile修饰的变量)。
- 指令重排序的目的是为了提高性能,指令重排序仅保证在单线程下不会改变最终的执行结果,但无法保证在多线程下的执行结果。
- 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止重排序。

浙公网安备 33010602011771号