JMM(java内存模型)
代码理解
public class test {
private static boolean f= false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->
{
while (!f)
{
//死循环
}
}).start();
Thread.sleep(1000);
new Thread(()->
{
System.out.println("修改状态");
f = true;
}).start();
}
}
发现第二个线程就算修改了f的值,线程1依然会无限循环。根据模型图可以分析运行过程
- 线程A先从主存中读取 f 的值,复制保存在自己的本地内存中
- 线程A根据本地内存中的 f 的值进行判断是否退出循环
- 线程B修改了主存中的值
- 但是线程A依然用的是本地内存中 f 的值进行判断,没有同步主存中的 f 值
八大原子操作
根据上面的案例,每一个读取,赋值给本地都是一个原子操作。分为
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
- use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
- 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
- 解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
针对代码例子可以画出如下的图
- 线程A read 主内存的值到一个临时区域(并不是工作内存)
- 线程A load 临时区域的值到工作内存并存储
- 线程A use 这个值进行操作
- 线程B read 主内存的值到一个临时区域(并不是工作内存)
- 线程B load 临时区域的值到工作内存并存储
- 线程B use 工作内存中的值
- 线程B assign 操作将工作变量的值修改
- 线程B store 一个临时区域
- 线程B write 最新的值到主内存
如何让boolean标记在线程B修改后让线程A感知到呢?可以使用volatile关键字
private static volatile boolean f= false;
可以这么理解
在主内存和线程中间有一个缓冲区域,每次数据在读取和写入的时候都会通过缓冲区域,线程A有一个类似在缓冲区域注册了一个监听事件。每当其他线程修改了值,write时,总要经过缓冲区域,这个时候会被监听事件捕获,这样线程A就知道了值被修改过,重新刷新工作内存中的值。
缓存一致性
如果开启了volatile在其他线程修改了值后,会马上进行store,write操作同步回主存。
new Thread(()->
{
System.out.println("修改状态");
f = true;
//todo 其他的耗时操作
}).start();
就算在修改值后有进行其他耗时的计算,f的值也会马上写入,让其他线程可见,可以更快的保证数据的一致性
volatile原理
在java翻译成汇编语句的时候会用一个指令lock标记
被标记的变量会
- 会将当前处理器缓存的数据立即写回主存
- 会开启类似总线缓存一致性的功能让其他cpu里缓存了变量的值失效。失效后其他cpu在使用时就会重新读取
- 提供内存屏障,禁止指令重排
并发编程三大特性
(volatile)就是保证了可见性和有序性(禁止指令重排),但是不保证原子性。
有序性和指令重排
指令重排是指在单线程的情况下,在不影响程序结果的前提下可以改变代码执行的顺序进行修改
x=1
y=1
可以优化为
y=1
x=1
但是在多线程环境中如果改变了运行顺序,很有可能结果就会有错误。
禁止指令重排的两个规则
- as-if-serial :不管怎么重排序(编译器和处理器为了提高并行度会进行重排序优化),单线程程序的执行结果不能被改变。
- happens-before (某些代码必须发生在某些代码之前)分为八个规则
dcl 单例锁问题
多线程下有问题的单例,多线程中可能得到一个不完整的对象
public class test {
// 单例
private static test t = null;
// 私有构造方法
private test() {
}
public static test getInstance1() {
if (null == t) {
synchronized (test.class) {
if (null == t) {
t= new test();
}
}
}
return t;
}
}
因为 new test()不是一个原子操作
步骤是
- 获取内容空间
- 初始化值(调用构造方法)
- 将值赋值给t
但是可能发生指令重排。顺序变为 1 3 2 ,如果是这样,第二个线程进来获取对象==null条件不成立,直接retrun对象,但是这个时候2的初始化并没有完成。所以可能会获得一个只有空间,但是所有元素都为空的对象。
解决办法就是加上volatile关键字,提供内存屏障,禁止指令重排
private static volatile test t = null;
JMM内存屏障
类似在读取或者写入直接加上一道墙,墙前墙后的指令不能进行指令重排序。