JUC高并发编程-共享模型之内存模型(JMM)

内存模型(JMM)

Java内存模型

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

JMM体现在以下几个方面

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

可见性

退不出的循环

先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:

1
2
3
4
5
6
7
8
9
10
11
12
13
static boolean run = true;

public static void main(String[] args){
Thread t = new Thread(()->{
while(run){
//...
}
});
t.start();

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

分析:

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

img84

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

    img85

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

img86

解决方法
1
volatile static boolean run = true;//加入volatile易变关键字

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

可见性和原子性

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

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

有序性

JVM会在不影响正确性的前提下,可以调整语句的执行顺序。

1
2
3
4
5
6
static int i;
static int j;

//在某个线程内执行如下操作
i = ...;
j = ...;

以上执行i,j的顺序不会影响最后结果。这种特性称之为指令重排,多线程下指令重排会影响正确性。

指令重排原理

鱼罐头的故事

加工一条鱼需要50分钟,只能一条鱼 ,一条鱼顺序加工。。。。

img87

可以将每个鱼罐头的加工流程细分为5个步骤:

  • 去鳞清洗 10分钟
  • 蒸煮沥水 10分钟
  • 加注汤料 10分钟
  • 杀菌出锅 10分钟
  • 真空封罐 10分钟

即使只有一个工人,最理想的情况:他能够在10分钟内同时做好5件事,因为对第一条鱼的真空装罐,不会影响对第二条鱼的杀菌出锅。

指令重排序列优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为:取指令-指令译码-执行指令-内存访问-数据写回这5个阶段。

img88

volatile原理

volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令后会加入读屏障
如何保证可见性

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存中

1
2
3
4
5
public void actor2(I_Result r){
num = 2;
ready = true;//ready是volatile赋值带写屏障
//写屏障
}

而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新的数据

1
2
3
4
5
6
7
8
9
public void actor1(I_Result r){
//读屏障
//ready是volatile读取值带读屏障
if(ready){
r.r1 = num +num;
}else{
r.r1 = 1;
}
}

img89

如何保证有序性

写屏障确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

1
2
3
4
public void actor2(I_Result r){
num = 2;
ready = true;//ready是volatile赋值带写屏障
}

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

1
2
3
4
5
6
7
8
9
public void actor1(I_Result r){
//读屏障
//ready是volatile读取值带读屏障
if(ready){
r.r1 = num +num;
}else{
r.r1 = 1;
}
}

img90

不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证知识保证了本线程内相关代码不被重排序

img91

double-checked locking问题

以著名的double-checked locking单例模式为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Singleton{
private Singleton{}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance(){
if(INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

以上实例实现的特点:

  • 懒惰实例化
  • 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外

happens-before

happens-before规定了共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结,抛开一下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见。

线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int x;
static Object m = new Object();

new Thread(()->{
synchronized(m){
x = 10;
}
},"t1").start();

new Thread(()->{
synchronized(,){
System.out.println(x);
}
},"t2").start();

线程对volatile变量的写,对接下来其它线程对该变量的读可见

1
2
3
4
5
6
7
8
9
volatile static int x;

new Thread(()->{
x = 10;
},"t1").start();

new Thread(()->{
System.out.println(x);
},"t2").start();

线程start前对变量的写,对该线程开始后对该变量的读可见

1
2
3
4
5
6
7
static int x;

x = 10;

new Thread(()->{
System.out.println();
},"t2").start();

线程结束前对变量的写,对其它线程得知它的结束后的读可见(比如其它线程调用t1.isAlive()或t1.join())等

1
2
3
4
5
6
7
8
9
static int x;

Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();

t1.join();
System.out.prinln(x);

线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int x;

public static void main(String[] args){
Thread t2 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.prinln(x);
break;
}
}
},"t2");
t2.start();

new Thread(()->{
sleep(1);
x = 10;
t2.interrupted();
},"t1").start();

while(!t2.isInterrupted()){
Thread.yield();
}
System.out.println(x);
}

对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

具有传递性,如果x hb->y并且y hb->z那么有x hb->z,配合volatile的防指令重排,有下面的例子

1
2
3
4
5
6
7
8
9
10
11
volatile static int x;
static int y;

new Thread(()->{
y = 10;
x = 20
},"t1").start();

new Thread(()->{
System.out.println(x);
},"t2").start();
-------------本文结束感谢您的阅读-------------