JMM之可见性介绍

一、Java内存模型

JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。

JMM 体现在以下几个方面

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受 cpu 缓存的影响
  • 有序性:保证指令不会受 cpu 指令并行优化的影响

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

二、不可见性导致的问题

示例

main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import lombok.extern.slf4j.Slf4j;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/27
* Description 退不出的循环
*/
@Slf4j(topic = "c.Test06")
public class Test06 {

static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (run){

}
});
t.start();

sleep(1);
log.debug("停止t线程...");
run = false; // 线程t不会如预想的停下来
}
}

运行结果

image-20230328232241773

从结果可以看出,当run变量为false后,t线程并未退出循环。

问题分析

1、初始状态,t线程刚开始从主内存读取了run 的值到工作内存。

image-20230328232254219

2、因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存 中,减少对主存中run的访问,提高效率

image-20230328232306676

3、1秒之后, main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变 量的值,结果永远是旧值

image-20230328232322029

三、可见性

3.1 volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import lombok.extern.slf4j.Slf4j;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/27
* Description 退不出的循环 -- volatile解决办法
*/
@Slf4j(topic = "c.Test06")
public class Test06 {

volatile static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (run){

}
});
t.start();

sleep(1);
log.debug("停止t线程...");
run = false;
}
}

运行结果

1
21:49:15.174 c.Test06 [main] - 停止t线程...

线程t在1s后正常退出了循环。

分析

当主线程修改主存中的run变量的时候,t线程一直访问的是自己缓存的run值,所以不认为run已经改为false,顾不会退出循环。

当为主存(成员变量)进行volatile修饰,增加变量的可见性, 当主线程修改run为false, t线程对run的值可见,这样就可以正常退出循环。

3.2 synchronized的可见性

synchronized的执行内部代码的过程分为五步,分别是:

  • 1、获得同步锁;
  • 2、清空工作内存;
  • 3、在主内存中拷贝最新变量的副本到工作内存;
  • 4、执行代码(计算或者输出等);
  • 5、将更改后的共享变量的值刷新到主内存中;
  • 6、 释放同步锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import lombok.extern.slf4j.Slf4j;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/27
* Description 退不出的循环 -- synchronized解决办法
*/
@Slf4j(topic = "c.Test07")
public class Test07 {

static boolean run = true;
private final static Object lock = new Object();

public static void main(String[] args) {
Thread t = new Thread(() -> {
while (run) {
synchronized (lock) {
if (!run){
break;
}
}
}
});

t.start();
sleep(1);
log.debug("停止t线程...");
synchronized (lock){
run = false;
}
}
}

运行结果

1
22:09:18.867 c.Test07 [main] - 停止t线程...

3.3 print打印输出

当在while循环代码中加入print打印输出时,t线程会退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import lombok.extern.slf4j.Slf4j;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/27
* Description 退不出的循环 -- print打印输出
*/
@Slf4j(topic = "c.Test06")
public class Test06 {

static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (run){
System.out.print("");
}
});
t.start();

sleep(1);
log.debug("停止t线程...");
run = false; // 线程t不会如预想的停下来
}
}

运行结果

1
22:10:28.651 c.Test06 [main] - 停止t线程...

分析

从print的源码中可以看出,print方法使用到了synchronized,synchronized可以保证原子性、可见性、有序性。当使用synchronized后,就会将线程的工作内存清空,在主内存中拷贝最新变量的副本到工作内存。所以会获取到正确的run值。

四、可见性 vs 原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线 程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。

上例从字节码理解是这样的:

1
2
3
4
5
6
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

比较一下之前讲线程安全时举的例子:两个线程一个 i++ 一个 i- -,只能保证看到最新值,不能解决指令交错。

1
2
3
4
5
6
7
8
9
10
11
// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0

getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。

总结

volatile关键字可以保证可见性、有序性,但是不能保证原子性。volatile具体原理见:volatile原理

synchronized关键字可以保证可见性、原子性,对于有序性:使用synchronized并不能解决有序性问题,但是如果是该变量整个都在synchronized代码块的保护范围内,那么变量就不会被多个线程同时操作,也不用考虑有序性问题!在这种情况下相当于解决了重排序问题!。


JMM之可见性介绍
http://example.com/2023/03/28/JMM之可见性介绍/
作者
程序员小魏
发布于
2023年3月28日
许可协议