1.概述
在Java虚拟机规范中,定义了Java内存模型(Java Memory Model,JMM)
,目的是为了屏蔽各种硬件和操作系统的内存访问差异。
2.计算机内存模型
由于计算机的存储设备与处理器的运算速度有着巨大的差距,所以现代计算机系统不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲。
基于告诉缓存的存储交互,解决了处理器与内存之间速度差的影响,但也引入了新的问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存,当多个处理器访问同一块主内存区域时,可能会导致各自的缓存数据不一致。
为了解决一致性问题,就需要各个处理器访问主内存需要遵循一些协议,例如MSI、MESI等,我们将其成为缓存一致性协议
。
3.Java内存模型
在Java虚拟机规范中,定义了Java内存模型(Java Memory Model,JMM)
,目的是为了屏蔽各种硬件和操作系统的内存访问差异。
在Java内存模型中,规定所有的共享变量都存储在主内存中,每条Java线程有自己的工作内存,工作内存中保存了主内存中共享变量的变量副本,线程对变量的读取、赋值都在自己的工作内存中进行。因此,各个线程对变量副本值的修改,对其它线程是不可见的,如果该共享变量存在多线程访问修改,就是线程不安全的。
3.1 主内存与工作内存的交互
将一个变量从主内存中拷贝到工作内存,再从工作内存同步回主内存,就是主内存与工作内存之间的交互。
Java内存模型中,定义了8种操作来完成主内存与工作内存间的交互。
操作类型 | 作用范围 | 作用 |
---|---|---|
lock | 主内存变量 | 将主内存变量标识为当前线程独占状态 |
unlock | 主内存变量 | 将当前线程锁定的变量释放出来 |
read | 主内存变量 | 将主内存变量值传输到工作内存中 |
load | 工作内存变量 | 将read操作得到的变量值放入工作内存的变量副本中 |
use | 工作内存变量 | 使用工作内存中的变量值 |
assign | 工作内存变量 | 为工作内存中的变量赋值 |
store | 工作内存变量 | 将工作内存变量值传输到主内存中 |
write | 主内存变量 | 将store操作得到的变量值放入主内存的变量值 |
同时,JMM也对上述8种操作定义了如下规则:
- (read和load操作)、(store和write操作)必须同时执行。
- 变量在工作内存中改变之后必须同步回主内存。
- 主内存的变量在同一时刻只允许一条线程对齐进行lock操作,但同一条线程可以重复执行多次lock操作,执行多次lock之后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对主内存的变量执行lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用该变量前,需要重新执行load、assign操作以初始化该变量的值。
- 如果主内存变量没有被lock操作锁定,那就不允许对其执行unlock操作。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
那么,Java线程会将工作内存的变量值同步回主内存呢?
- 线程中释放锁,会同步变量回主内存。
- 线程上下文切换。
- CPU有空闲时间,例如,线程休眠、IO操作等。
3.2 原子性、可见性、有序性
3.2.1 原子性
保证对变量简单的读取、赋值操作是原子性的。例如
int i = 2
。
3.2.2 可见性
当一个线程修改了共享变量的值时,其它线程能够立即得知这个修改。
那么如何才能保证可见性呢?
-
volatile
轻量级的同步机制,可以保证共享变量在线程间的可见性,但是不能保证线程安全
-
synchronized
-
Lock加锁
3.2.3 有序性
Java程序在单线程环境下,能够保证操作具有天然的有序性;而在多线程环境下,所有的操作都是无序的。Java语言提供了volatile和synchronized两个关键字以及Lock加锁来保证线程之间操作的有序性
3.3 Happens Before原则
Happens Before原则是判断线程是否安全的非常有用的手段。
Java内存模型下存在一些天然支持Happens Before原则的条件,这些条件无需任何同步器协助就已经存在,可以在代码中直接使用。如果两个操作之间满足这些条件,则它们就没有顺序性保障。
-
程序次序规则
在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。适用于单线程环境。
-
监视器锁定规则
一个锁的unlock操作先行发生于该锁的lock操作。 -
volatile变量规则
对一个volatile变量的写操作先行发生于后面对该变量的读操作。 -
线程启动规则
Thread对象的start()方法先行发生于该线程的每一个动作。 -
线程中断规则
对线程interrupt()方法的调用先行发生于代码中对中断状态的检测。 -
线程终止规则
线程中的所有操作都先行发生于对此线程的终止检测,例如Thread.join()方法。 -
对象终结规则
一个对象的初始化操作先行发生于它的finalize()方法的开始。 -
传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
3.4 线程安全实战
private int value = 0;
pubilc void setValue(int value){
this.value = value ;
}
public int getValue(){
return value;
}
假设线程A先调用了setValue(1);然后线程B调用了同一个对象的getValue()方法,那么线程B得到的返回值是什么呢?
首先根据Happens Before原则的规则进行匹配,当前场景是否满足这些条件。
- 当前是多线程环境,因此程序次序规则不适用
- 没有同步代码块,自然不会发生unlock和lock操作,监视器锁定规则不适用
- value变量没有被volatile关键字修饰,volatile变量规则不适用
- 其他几个规则都与当前场景无关
综上所述,当前的操作是线程不安全的。
解决方案:
- 可以使用volatile修饰value变量,由于set方法对value的修改不依赖value的原值,满足volatile关键字使用场景,就可以套用volatile变量规则来实现Happens Before原则
- 使用synchronized关键字修饰get/set方法,套用监视器锁定规则来实现Happens Before原则