目录
- 一、是什么
- 二、为什么要有内存模型?
- 三、内存交互的八个原子操作
- 四、操作规则
- 五、内存可见性
- 六、解决内存可见性问题
一、是什么
Java多线程内存模型是基于Cpu缓存模型建立的,它的作用是屏蔽掉不同硬件和操作系统的内存访问差异,实现各种平台具有一致的并发效果。
二、为什么要有内存模型?
画一个简单的CPU缓存模型。
开始CPU是直接和主存进行交互的,但这样会有一个很大的问题,CPU的计算速度非常快,而主存是硬盘操作,比起CPU会慢很多很多,有时候CPU需要等待主存,导致效率很低。所以在CPU和主存之间加一个高速缓存作为缓冲,虽然高速缓存和CPU之间还存在速度差别,但比直接访问主存的效率高
的多。
注:这里的高速缓存是分成多级缓存,这里只是了解,简单画了一下。
三、内存交互的八个原子操作
线程从主内存中读取一个变量到自己的工作内存,线程结束时再把这个变量写回主内存中,是需要经过以下8个操作。(按顺序)
- lock(锁定):将一个变量标识成线程独有状态。
- read(读取):将变量的值读取到线程的工作内存中。
- load(加载):将读取到的值指向工作内存中变量副本。
- use(使用):把工作内存中的一个变量值传递给执行引擎。
- assign(赋值):将执行引擎收到的值赋值给变量副本。
- store(存储):将变量副本的值传递到主内存。
- write(写入):将值赋值给主内存的变量。
- unlock(解锁):将锁定的变量解锁。
这些操作需要按顺序执行,可以不连续执行,比如read和load之间可以夹杂其他操作。lock可选。
四、操作规则
- 如果一个变量在线程中改变了,就必须同步到主内存中。
- 如果一个变量没有发生过任何assign操作,就不需要往主内存写了。
- 不运行read、load和store、wirte单独出现。
- 一个变量只能同时被一个线程锁定,这个线程可以进行多次lock锁定,但必需对应次数的unlock解锁。
- 对一个变量执行lock操作,那将会清空工作内存中此变量的值,需要重新load或assign。
- unlock前必需store和write。
- 不运行unlock未被lock或者被其他线程lock的变量。
- 这些操作需要按顺序执行,可以不连续执行。
五、内存可见性
在内存模型中,线程会把内存中的变量a的值读取到自己的工作内存中,并赋值给工作内存中的副本变量,使用完之后再把新值写会内存,更新内存中变量a的值。
这样就会导致一个问题,看下面代码:
public class Demo1 {
private static int a = 1;
public static void main(String[] args) {
new Thread(() -> {
while (a == 1) {}
System.out.println("a不等于1了....");
}, "线程2").start();
new Thread(() -> a = 2, "线程1").start();
}
}
代码比较简单,一个线程判断a的值,一个线程改变a的值,思考一下,会不会执行 System.out.println("a不等于1了....");
答案:不会打印。看下图
线程1和线程2都用到了变量a,线程1将改变了a的值,并将a的值写回了主内存(不管怎样,线程结束之前肯定要写回),但线程1的这个操作对线程2来讲是不可见的,也就是线程2根本感知不到线程1修改了a的值,线程2中的a还是1,会一直while下去。这就是内存可见性。
这也会产生另一个问题,如果两个线程都是修改操作,就会导致一个线程会覆盖另一个线程刚修改好的值。这个问题在多线程数值计算时会造成错误的结果。
六、解决内存可见性问题
- 加锁
我们以synchronized锁为例,其他锁也可以。
在获取某个变量的锁的时候,会清空工作内存,重新将这个变量从主内存加载到工作内存中。
public class Demo1 {
private static Integer a = 1;
public static void main(String[] args) {
new Thread(() -> {
while (a == 1) {
synchronized (a) {
}
}
System.out.println("a不等于1了....");
}, "线程2").start();
new Thread(() -> a = 2, "线程1").start();
}
}
- volatile关键字
private static volatile int a = 1;
,将变量用这个关键字修饰,就可以保证另一个线程修改这个值时对当前线程可见。
简单说一下原理,jdk底层是c++实现的,底层代码中给变量赋值的时候,会判断这个变量是否是被volatile修饰的,如果是会在这个代码之后执行一个lock指令,这个指令不是内存屏障但有内存屏障的功能,它作用就是锁住这个变量的内存地址,并且立马将线程1中修改的值写回主存,线程2的cpu通过嗅探机制判断总线上是否有cpu要修改自己缓存中a的内存地址,如果有会将自己缓存中的值变成失效状态,下次在使用的时候,发现是失效状态的,会重新从主存中读取加载。