【详解】Java多线程之内存模型三大特性
目录
一、数据不一致的问题
- CPU运行速度比较快,每次去内存读取数据就比较耗时
- 所以,CPU与内存之间会存在一个高速缓存来提高CPU的效率
- 这个高速缓存存放着从内存中读取的数据
当一个线程仅仅对数据是读取的操作,Java会认为该线程不需要刷新缓存
。当有另一个线程,对数据进行修改时,第一个线程不可见,出现了数据不一致的问题
JMM模型
代码演示
public class Volatile {
private static int INIT_VALUE = 0;
private final static int MAX_LIMIT = 5;
public static void main(String[] args) {
new Thread(() -> {//负责修改数据
int localValue = INIT_VALUE;
while (localValue < MAX_LIMIT) {
System.out.println("数值即将更新为" + ++localValue);
INIT_VALUE = localValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "UPDATER").start();
new Thread(() -> {//新建一个读取数据的线程
int localValue = INIT_VALUE;
while (localValue < MAX_LIMIT) {
if (localValue != INIT_VALUE) {//当数据发生更改
System.out.println("数据现在为" + INIT_VALUE);
localValue = INIT_VALUE;
}
}
}, "READER").start();
}
}
结果:
数值即将更新为1
数值即将更新为2
数值即将更新为3
数值即将更新为4
数值即将更新为5
二、解决方法
解决方法有两种方法:
给数据总线加锁
- CPU和其他部件进行通信都是通过总线来进行的,,如果对总线加LOCK#锁的话,也就是说
阻塞了其他CPU对其他部件访问(如内存)
- 由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
CPU高速缓存一致性协议
例如 Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的,它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
三、JMM模型中的三个概念
原子性
- 一个操作或者多个操作要么全部执行成功,要么全部执行失败
- 类似于数据库理论中的原子性
比如
i = 9 时 :
9 的二进制为1001
执行赋值的操作时必须保证二进制的每一位都赋值成功
不能出现: i = 1000 或者 i = 0001
可见性
- 当多个线程修改同一个变量时,其他线程必须要能感知到
- 主要需要解决
缓存不一致的问题
有序性
- 保证代码的顺序
- 这个主要避免Java优化器的重排序(只要求最终的一致性)
比如:
----------Thread-1----------------
init = true 2;
obj = createObj() 1;
----------Thread-2----------------
while(!init){
sleep(100);
}
useTheObj(obj);
----------------------------------
- 在多线程时,出现了重排序让线程1的语句2执行在语句1的前面
- 此时线程2使用该对象时,就会出现空指针异常
四、Java中对三个特性的保证
保证原子性
- 对于
基本数据类型的变量读取和赋值
是保证了其原子性,这些操作不可中断
分析
i = 10; 原子性
b = a ; 不满足,1.load a,2. assign b
c++; 不满足,1.load c,2. add ,3. assign c
c = c+1;不满足,1.load c,2. add, 3. assign c
下面以cnt++
举例
- 由此可以发现:对 int 类型读写操作满足原子性只是说明 load、assign、store 这些
单个操作
具备原子性 - 注:Java对64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,
即 load、store、read 和 write 操作可以不具备原子性。
保证可见性
使用volatile关键字
- 但是不能保证其原子性
- 所有的线程都会直接去内存拿数据,而不再使用高速缓存,避免了缓存不一致的情况
synchronized
- 对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
final
- 被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
保证有序性(happens-before relationship)
1. 单一线程原则
在一个线程内
,在程序前面的操作先行发生于后面的操作
2. 管程锁定规则
- 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
3. volatile 变量规则
- 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
4. 线程启动规则
- Thread 对象的 start() 方法调用先行发生于此线程的每一个动作
5. 线程加入规则
- Thread 对象的结束先行发生于 join() 方法返回。
6. 线程中断规则
- 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,
可以通过 interrupted() 方法检测到是否有中断发生
7. 对象终结规则
- 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
8. 传递性
- 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C
五、volatile关键字
一个共享变量被volatile修饰,具备两层语义
强制对缓存的修改操作立刻写入到主存中,并且会使其他线程的缓存失效
。保证了线程间的可见性- 禁止对其进行重排序,
不会把后面的指令放在前面,也不会把前面的指令放在后面
。也就是保证了有序性 - 并未保证原子性
public class Volatile {
private static volatile int INIT_VALUE = 0;
private final static int MAX_LIMIT = 5;
public static void main(String[] args) {
new Thread(() -> {//负责修改数据
int localValue = INIT_VALUE;
while (localValue < MAX_LIMIT) {
System.out.println("数值即将更新为" + ++localValue);
INIT_VALUE = localValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "UPDATER").start();
new Thread(() -> {//新建一个读取数据的线程
int localValue = INIT_VALUE;
while (localValue < MAX_LIMIT) {
if (localValue != INIT_VALUE) {//当数据发生更改
System.out.println("数据现在为" + INIT_VALUE);
localValue = INIT_VALUE;
}
}
}, "READER").start();
}
}
六、volatile使用场景
1. 状态量的标记
volatile boolean flag = true;
while(flag){
//....
}
.....
- 因为线程只存在读的操作,所以加了volatile可以让线程读取的数据是内存中最新的数据
2. 保证屏障前后的一次性
private boolean volatile init = false
----------Thread-1----------------
obj = createObj() 1;
..... 2;
init = true 3;
.... 4;
.... 5;
----------Thread-2----------------
while(!init){
sleep(100);
}
useTheObj(obj);
----------------------------------
- 可以保证1、2一定在3执行之前,4、5一定再3执行之后,但是不能保证1、2和3、4的顺序
七、重排序的验证
以下代码可以验证Java的重排序
public class Main {
/** * 这是一个验证结果的变量 */
private static int a = 0;
/** * 这是一个标志位 */
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
//由于多线程情况下未必会试出重排序的结论,所以多试一些次
for (int i = 0; i < 1000000; i++) {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
//这里等待线程结束后,重置共享变量,以使验证结果的工作变得简单些.
threadA.join();
threadB.join();
a = 0;
flag = false;
}
}
static class ThreadA extends Thread {
public void run() {
a = 1;
flag = true;
}
}
static class ThreadB extends Thread {
public void run() {
if (a == 0 && flag) {
System.out.println("ha,a==0");
}
}
}
}
- 按照代码顺序,如果执行到flag = true,此时a一定为1
- 但是,线程二依然会输出结果
- 原因在于flag = true和 a=1 的
出现了重排序
在 happens-before 原则下
- 线程A的无论怎么进行重排序,都不会影响到线程A最终的结果
- 但是,
在共享变量与多线程的情况下
,重排序导致线程B对资源的访问出现的BUG