JMM

JMM

JMM内存模型

Java Memory Model

 java内存模型,用来屏蔽操作系统和各种硬件的内存访问差异,以实现Java程序在各种平台下运行都能达到一致的内存访问效果

​ Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

​ 不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

image-20230613145023764

注意:这里所说的主存和计算硬件主存相似,但是java的这片区域是虚拟机的一部分

JMM定义了什么

JMM围绕了三个特征建立起来的,分别是原子性、可见性、有序性,这三个特征是java并发的基础

原子性

定义:原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。

1
2
3
int i = 0;
i++;
i = i+1;

第一个操作是原子性的,基本复制语句;

第二个,多个线程会发生竞态,Check then Act 先检查后执行,i++是四个字节码指令,先读取i值,然后再对+1操作,再将i写回到内存中

第三个和第二个一样,都不是原子操作

可见性

​ 可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。

​ 除了volatile关键字之外,final和synchronized也能实现可见性。

​ synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。

​ final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。

案例

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
package Thread.Day04;

import java.util.concurrent.TimeUnit;

/**
* @author MR.XSS
* @version 1.0
* ==========> volatile 对共享变量可见性进行操作
* 2023/6/13 9:31
*/
public class Demo01 {
static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){

}
}).start();


TimeUnit.SECONDS.sleep(1);
System.out.println("停止运行");
flag = false;
}
}

结果

​ 出现这种结果和java的内存模型有关当多次对一个变量进行读取的时候,java会将数据读取到高速缓存区,这样就会导致变量的不可见,当外部对该变量进行修改的时候,线程内部是无法看到的,导致了运行出现了死循环。

image-20230613120342207

分析

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

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

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

img

解决方案

  • 使用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
package Thread.Day04;

import java.util.concurrent.TimeUnit;

/**
* @author MR.XSS
* @version 1.0
* ==========> volatile 对共享变量可见性进行操作
* 2023/6/13 9:31
*/
public class Demo01 {
static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){

}
}).start();


TimeUnit.SECONDS.sleep(1);
System.out.println("停止运行");
flag = false;
}
}
  • 使用synchronized同步,同样也可以维持正常

结果

image-20230613121619873

synchronized和volatile对比

  • synchronized用于修饰方法或者代码块,volatile只能修饰变量。

  • synchronized保证操作的原子性,同时保证变量的可见性,volatile保持变量的可见性。

  • synchronized通常适用于写多读少的场景,会造成线程阻塞,volatile通常适用于写少读多的场景,不会造成线程阻塞。

有序性

指令重排序

1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

使用volatile关键字和synchronized关键字可以保证不会指令重排

volatile关键字,实现禁止重排序原理是实现了内存读写屏障

volatile原理

  • 写屏障保证在该屏障之前的,对共享变量进行的改动,都会同步到主存当中
  • 而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据