线程安全之可见性揭秘-《云课堂》

Badia ·
更新时间:2024-11-10
· 680 次阅读

线程安全之可见性问题 JVM运行时数据区和JMM(java内存模型)有什么区别? 1. JVM运行时数据区 是 由 《==Java虚拟机规范==》定义的 理解:不同的JVM厂商都必须 遵循 《==Java虚拟机规范==》进行开发 2. JMM是由 《java语言规范》定义的 理解:每一种语言都有自己的 解释器或编译器 什么是可见性问题?

我们先来看一段程序,如下:
在这里插入图片描述
大家猜猜这个程序运行结果是什么样子? 好了,不卖关子啦,我这里已经运行过了,如下图:
在这里插入图片描述
我们可以看到,【i】的值没有打印,而且线程还没有执行结束,那是为什么呢? 那我们一起来一步一步的分析推理下,我们先把JMM逻辑图画一下,如下图:
在这里插入图片描述

main线程和子线程都读取了 主内存中 isRunning=true 对应图中的 (1)、(2) main线程更改主内存 isRunning = true 子线程没有立刻读取到 主内存变化的 值,导致线程可见性问题

接下来我们一起分析下:
导致可见性问题只有两种可能:

main线程没有将新值写入主内存;

main将新值成功写入了主内存,子线程没有读取到;

不管是哪一种,我们有理由猜测是由于在工作线程和主内存之间存在的 CPU高速缓存导致,是高速缓存的锅,那我们再分析下:
如果是高速缓存的锅,那为什么在main线程sleep了3秒(我们都知道CPU的高速缓存运行速度比内存要快的多)之后,子线程读到isRunning还是true呢?我们有理由猜测,不是CPU高速缓存的锅,那会是谁呢?
接来我们通过下图接着分析:
在这里插入图片描述
javac 是编译前只是将.java文件编译成.class字节码文件,而不是机器码,CPU不会执行代码,有理由猜测不是javac的锅,根据排除法,最后我们有理由锁定是JIT编译器的锅,那我们来看下JIT编译器做了什么事情:
通过云课堂学习,我们得知 JIT编译器在执行的时候会 遵循as-if-serial语义,会对代码进行指令重排(即优化提升性能),就如我们上图看到的JIT编译器代码的样子,到此真凶已经找到,那怎么解决呢?请看《可见性问题解决方案》

可见性问题解决方案

对多线程稍微有接触的童鞋都知道 用volatile关键字解决,那我们来揭开它的面纱看一下:

我们用javap -v -p demo.class 反编译下 刚才执行的.class文件,如下图:
在这里插入图片描述

我们看到 用volatile修饰的isRunning变量反编译后 看到访问控制加了个ACC_VOLATILE标识符,我们到Oracle官网看下这个标识符的解释:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
在这里插入图片描述

我们看到 cannot be cached 翻译过来就是不能被缓存,这里指的就是不允许JIT编译器进行缓存,到这里volatile神秘面纱就揭开了,到此我们也整理下volatile关键字吧

可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。 java内存模型规定: 对volatile变量v的写入,与所有其他线程后续对v的读同步 要满足这些条件,所以volatile关键字就有这些功能: 禁止缓存: volatile变量的访问控制符会加个 ACC_VOLATILE 对volatile变量相关的指令不做重排序
作者:weixin_41357524



可见性 云课堂 线程安全 线程

需要 登录 后方可回复, 如果你还没有账号请 注册新账号