前言:

本文内容:理解JMM、Volatile可见性及非原子性、指令重排详解

推荐免费JUC并发编程视频:【狂神说Java】JUC并发编程最新版通俗易懂_哔哩哔哩_bilibili

理解JMM

概述

JMM:Java内存模型,并不存在,约定!

作用:Java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

关于JMM同步的约定

  1. 线程解锁前,必须把共享变量立刻刷回主内存
  2. 线程加锁前,必须读取主内存中的最新值到工作内存中
  3. 加锁和解锁时同一把锁

JMM八种内存交互操作

17

  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对八种内存交互制定的规则

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有assign的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

简单测试

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
package com.jokerdig.tvolatile;

import java.util.concurrent.TimeUnit;

/**
* @author Joker大雄
* @data 2022/8/26 - 11:10
**/
public class JMMDemo {
private static int number = 0;
public static void main(String[] args) { // main线程

new Thread(()->{// 线程1
while(number==0){

}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 虽然number值改变,并返回到主内存,但是线程1还没接收到新的值,所以一直在循环
number=1;
System.out.println(number);

}
}

运行结果

输出结果为1,但程序没有停止

1
1

问题:线程1不知道主内存中的值已经被修改!

Volatile可见性及非原子性

概述

Volatile是Java虚拟机提供的轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

可见性

解决程序无法停止问题

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
package com.jokerdig.tvolatile;

import java.util.concurrent.TimeUnit;

/**
* @author Joker大雄
* @data 2022/8/26 - 11:10
**/
public class JMMDemo {
// 增加关键字volatile,保证可见性
private volatile static int number = 0;
public static void main(String[] args) { // main线程

new Thread(()->{// 线程1
while(number==0){

}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 虽然number值改变,并返回到主内存,但是线程1还没接收到新的值,所以一直在循环
number=1;
System.out.println(number);
}
}

运行结果

当值被改变,程序直接停止。

1
2
3
1

Process finished with exit code 0

非原子性

原子性:不可分割

线程在执行人物的时候,不能被打扰和分割,要么同时成功,要么同时失败。

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
package com.jokerdig.tvolatile;

import javax.xml.ws.soap.Addressing;

/**
* @author Joker大雄
* @data 2022/8/26 - 11:22
**/
// 非原子性
public class VDemo {
private static int number = 0;

public static void add(){
number++;
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+number);
}
}

运行结果

默认情况,结果都不同,很难到达20000

1
2
3
main 19605

Process finished with exit code 0

添加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
27
28
29
30
31
package com.jokerdig.tvolatile;

import javax.xml.ws.soap.Addressing;

/**
* @author Joker大雄
* @data 2022/8/26 - 11:22
**/
// 非原子性
public class VDemo {
// 添加volatile
private volatile static int number = 0;

public static void add(){
number++;
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+number);
}

}

运行结果

添加volatile,结果也都不同,很难到达20000,证明volatile不能保证原子性

1
2
3
main 19609

Process finished with exit code 0

解决办法:加锁即可!

拓展

如果不加锁,该如何解决这个问题?

分析

从底层代码看出,number++并不是原子性操作!由三步(获得值,执行+1,写回这个值)来完成!

解决

使用原子类,解决原子性问题!

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
package com.jokerdig.tvolatile;

import javax.xml.ws.soap.Addressing;
import java.util.concurrent.atomic.AtomicInteger;

/**
* @author Joker大雄
* @data 2022/8/26 - 11:22
**/
// 非原子性
public class VDemo {
// 原子类 AtomicInteger
private volatile static AtomicInteger number = new AtomicInteger();

public static void add(){
// number++; // 非原子性操作
number.getAndIncrement(); //AtomicInteger+1方法 底层CAS
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+number);
}
}

运行结果

1
2
3
main 20000

Process finished with exit code 0

指令重排详解

什么时指令重排

计算机在执行程序时候,为了提高代码、指令的执行效率,编译器和处理器会对指令进行重新排序,一般分为编译器对于指令的重新排序、指令并行之间的优化、以及内存指令的优化。

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

1
2
3
4
5
6
int x =1; // 1
int y =2; // 2
x = x + 5; // 3
y = x * x; // 4
我们所期望步骤:1234,但可能执行的时候变为:21341324
但是不可能为:4123

但是对最终结果不造成影响!

可能造成影响的结果

a b x y默认都为0

线程A 线程B
x = a y = b
b = 1 a = 2

正常的结果:x=0,y=0

重排可能导致

线程A 线程B
b = 1 a = 2
x = a y = b

重排的结果:x=2,y=1

Volatile避免指令重排

内存屏障,CPU指令。作用:

  1. 保证特定操作的执行顺序
  2. 可以保证某些变量的内存可见性

这些特性实现了volatile的可见性,添加volatile就会在执行前后各添加内存屏障来避免指令重排。