Day11 多线程进阶 7 JMM Volatile
uwupu 啦啦啦啦啦

JMM

Java Memory Model

介绍

JMM:Java内存模型。(一个约定,协议)

使用场景 / 解决的问题

在不同硬件生产商和不同操作系统下,内存的访问逻辑有一定的差异,导致一段代码在某个系统环境下运行良好,并且线程安全,但是换了系统就出现各种问题。

Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能达到相同的访问效果。

概念:内存划分

JMM规定内存主要划分为主内存和工作内存两种

此处的主内存和工作内存与JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,若非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域。

从底层来看,主内存对应的是硬件的物理内存工作内存对应的是寄存器和高速缓存

概念:内存交互操作

内存交互操作有8中,虚拟机实现必须保证每一个操作都是原子的(原子:即不可再分的)。

原子操作 说明
lock(锁定) 作用于主内存的变量,把一个变量标识为线程独占状态。
unlock(解锁) 作用于主内存的变量,把一个处于锁定状态的变量释放出来。
read(读取) 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中。与load一起使用。
load(载入) 作用于工作内存的变量,把 read操作从主存中传输的变量 放入工作内存中。与read一起使用。
use(使用) 作用于工作内存的变量,把工作内存的变量传输给执行引擎。每当虚拟机遇到一个需要使用到变量的值,就要用到这个指定。
assign(赋值) 作用于工作内存的变量,把一个从执行引擎中接受到的值放入工作内存的变量副本中。
store(存储) 作用于主内存中的变量,把一个从工作内存变量的值传输到主内存中。与write一起使用。
write(写入) 作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。与store一起使用。

JMM原子操作的一些约定:

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

概念:一些约定

关于JMM的一些同步的约定

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

Volatile

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

介绍

hole…

作用/特征

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

应用场景

单例模式。

特征介绍

特性1:保证可见性

问题简述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//出现问题:num赋值为1后,线程并没有停止
public class Demo29_Volatile {
private static int num = 0;//位于主存的一个变量
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num == 0){
//在线程中使用num时,会读取到工作区的num值,并没有同步主存的num值,
//当主线程修改了主存中的num值,线程仍旧不会停止
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;//修改num值为1
System.out.println("现在num为1");
}
}

当num赋值为1,子线程没有停止。因为在当前程序中,子线程运行后,会将num值复制到工作区,后续线程使用num值,只会使用到工作区的num,不使用主线程的num,主线程修改num后,子线程工作区的num仍为原来的值,导致在线程内num一直为0,子线程不会停止。

另外的说法:在子线程运行后,num被read/load到子线程的工作区后,线程的执行引擎使用工作区的num。当主线程将num值修改后,子线程工作区的num仍为原来的值,线程不会停止。

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//出现问题:num赋值为1后,线程并没有停止
//解决方法:在num前加volatile
public class Demo29_Volatile {
private volatile static int num = 0;//位于主存的一个变量
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num == 0){
//在线程中使用num时,会读取到工作区的num值,并没有同步主存的num值,
//当主线程修改了主存中的num值,线程仍旧不会停止
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;//修改num值为1
System.out.println("现在num为1");
}
}

在num前加:volatile。

待完善 volatile可以保证可见性。

特性2:不保证原子性

基本问题

代码:

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
public class Demo29_Volatile不保证原子性 {
//volatile不保证原子性
private volatile static int num = 0;
public static void add(){
num++;
}

public static void main(String[] args) {
//理论上num结果应为20000
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
},String.valueOf(i)).start();
}

//等待上面创建的线程执行结束
while (Thread.activeCount()>2){
//Java有两个默认线程:main和gc。所以这里写2。
Thread.yield();//让主线程从运行态变为就绪态
}
System.out.println(Thread.currentThread().getName()+" "+num);//输出结果
//main 18821
}
}

在当前程序中,num输出结果并不是20000。

如何解决这个问题?

方法1: 在add()方法前添加synchronized,或添加lock机制。

方法2: 将int替换为AtomicInteger原子Integer类。

使用原子类解决这个问题:将int替换为AtomicInteger原子Integer类。

分析add()的字节码

image

add()里的“num++”,是线程不安全的。从底层看,”num++”不是一个原子性操作

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

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
public class Demo29_Volatile不保证原子性_从原子角度解决问题 {
// volatile不保证原子性
// 将int改为AtomicInteger原子类Integer。
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
//num++;//不是一个原子性操作
num.getAndIncrement();// AtomicInteger + 1 方法 底层:CAS
}

public static void main(String[] args) {
//理论上num结果应为20000
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
},String.valueOf(i)).start();
}

//等待上面创建的线程执行结束
while (Thread.activeCount()>2){
//Java有两个默认线程:main和gc。所以这里写2。
Thread.yield();//让主线程从运行态变为就绪态
}
System.out.println(Thread.currentThread().getName()+" "+num);//输出结果
//main 20000
}
}

AtomicInteger

类的底层直接与操作系统挂钩!在内存中操作值。

hole….

特性3:禁止指令重排

介绍

Volatile基于内存屏障,可以避免指令重排。

实现原理

在正常的指令流中,xx会为volatile指令前后添加内存屏障,禁止volatile前后的指令顺序交换。

依赖

内存屏障 作用:

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

依赖于“内存屏障”,volatile实现了禁止指令重排。

其他

指令重排

介绍

计算机不按照写的程序的顺序去执行。

可能触发指令重排

  1. 编译器优化的重排
  2. 指令并行可能会重排
  3. 内存系统重排

一个例子

1
2
3
4
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4

我们期望的: 1234

实际执行可能的存在: 1324 、 2134等

不可能的存在:4123

查看class文件字节码

javap -c 类名.class

原子性操作

原子,即不可再分。

 评论