从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用
环境
OS | Win10 |
CPU | 4核8线程 |
IDE | IntelliJ IDEA 2019.3 |
JDK | 1.8 -server模式 |
JVM被设置成-server模式的意义
其中之一是为了线程的执行效率,从线程的私有内存中读取变量,而不是从主存中获取;
比如主存中有个变量A,第一次线程从主存中取得A变量的值后,会复制到自己的私有内存中,以后也会从自己的私有内存中取A变量的值,那么主存中的A被更改,则无法及时获取,这时候就需要让A变量在内存可见。
场景
最初的代码
一个线程A根据flag的值执行死循环,另一个线程B只执行一行代码,修改flag的值,让A线程死循环终止。
Visbility.java
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- flag = true;
- }
- }
Main.java
- public class Main {
- public static void main(String[] args) {
- Visbility visbility = new Visbility();
- Thread cyclic = new Thread(visbility::cyclic);
- Thread setter = new Thread(visbility::setter);
- cyclic.start();
- setter.start();
- }
- }
多次执行Main函数结果:程序很快就终止。
这是为什么呢?我没有让flag值在多线程之间内存可见呀,怎么线程setter修改flag后,cyclic线程获得了修改后的flag终止死循环?先带着疑问。
添加for循环耗时代码
接着,在setter方法里,在修改该flag之前,添加一行耗时代码(用for循环,为什么不用TimeUnit,后面会说到),此时Visbility.java如下:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- for (int i = 0; i < 999999; i++) ;
- flag = true;
- }
- }
多次执行Main函数结果:程序一直不结束。
这是为什么呢?难道执行个循环99999次,CPU永远执行不完导致flag的值无法被修改该吗?还是说内存可见性的问题?
用volatile解决内存可见性
我们给flag加上volatile关键字进行修饰(后面有其他的方式如锁,System.out.println -_- 解决变量内存及时可见性),Visibility.java代码如下:
- public class Visbility {
- private volatile boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- for (int i = 0; i < 999999; i++) ;
- flag = true;
- }
- }
多次执行Main函数结果:程序几百毫秒后终止。
看来确实存在内存可见性的问题,线程cyclic获取到了setter线程修改后的flag并终止,解决内存可见性的方式特别多,后面再列几种;
但是结果证明了,并不是CPU执行不完了999999次的循环,而且是很快的执行完,那为什么和最初什么都没加的代码相比,加上了这99999次循环的耗时,就必须要加上volatile才能让setter线程中的flag的值被cyclic线程感知。
去掉volatile,减少for循环次数,减少耗时
继续修改代码,去掉volatile,并把for循环的次数999999减少至99999(大家不同的机器不同的环境可能需要设置不同数值),Visbility.java代码如下:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- for (int i = 0; i < 99999; i++) ;
- flag = true;
- }
- }
多次执行Main函数结果:程序几百毫秒内结束。
这里我去掉了volatile关键字,仅仅减少了setter线程修改flag之前模拟的for循环耗时,结果似乎又flag内存可见了(cyclic死循环线程终止)。
总结上面的几中情况
当setter线程修改flag之前无任务和耗时相对较短的任务时,不需要volatile修饰flag变量,cyclic线程能获得被setter修改该后的flag值;
当setter线程修改该flag之前有耗时相对较长的任务时,需要volatile修改flag变量,cyclic线程才能获得被setter修改该后的flag值。
几种猜想(暂未证明)
1. 在皮秒级内(这也是为什么我这里模拟耗时用for循环,而不用TimeUnit,因为TimeUnit最小的单位是纳秒,开始我使用最小的单位时间TimeUnit.NANOSECONDS.sleep(1),多次执行程序,每次结果都是一直都不结束,所以我需要更小的耗时时间),JVM已经感知到"flag"被修改,所以两个线程都获取的主存的值,第一个线程的循环终止
2. 由于setter线程的任务实在是太小(联想到了进程调度算法),所以setter在极短时间内被CPU执行完后,线程cyclic也立刻被同一个CPU执行,即取的是同一块本地内存(CPU高速缓存)
3. 由于setter线程的任务实在是太小(联想到了进程调度算法),所以setter在极短时间内被CPU执行完后,值已经被刷新到主存,cyclic获得的是主存中最新的值
本来想验证下第二种猜想,查了下,暂时无法简单的通过Java类库代码来获取当前线程是被哪个CPU执行(JNA+本地安装对应的Library:https://github.com/OpenHFT/Java-Thread-Affinity);
耗时任务的意义
有了这个耗时任务,如果上面的cyclic已经启动了,JVM感知到(在耗时任务执行过程中,CPU早已做了多次运算了),除了cyclic这个线程以外,没有其他线程在操作"flag", JVM会假设"flag"的值一直都没有被改变,所以cyclic线程一直从自身线程本地内存中获取值(在未使用synchronized, volatile等实现"flag"的内存可见性时) ,所以就算setter线程修改"flag"的值,cyclic还是从自己的线程的本地内存中读取。
如何保证变量在内存中及时可见?
主要有两种,一种是用volatile,一种是锁;
还有Atomic Class?底层value也是用的volatile,以及sun.misc.Unsafe:https://www.cnblogs.com/theRhyme/p/12129120.html;
当然AQS也是volatile+sun.misc.Unsase。
Volatile保证变量在内存中及时可见
至于volatile例子上面已经写了,JAVA内存模型中VOLATILE关键字的作用:https://www.cnblogs.com/theRhyme/p/9396834.html
用锁来保证内存的可见性
锁有很多很多种,所以实现的方式也有很多,这里列几种有趣的实现,比如System.out.println也能保证能保证内存可见性?
System.out.println的形式
首先我们把setter修改flag之前添加耗时任务(仅66纳秒)TimeUnit.NANOSECONDS.sleep(66),即确保不触发刚才的猜想:
- import java.util.concurrent.TimeUnit;
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
执行结果和之前一样:多次执行Main函数,每次都不结束。
然后我们在cyclic死循环里添加一行输出语句:System.out.println,不加volatile关键字修饰flag,此时Visibility.java如下:
- import java.util.concurrent.TimeUnit;
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- System.out.println(flag);
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
多次执行Main函数的结果:都是输出了几十个false后程序终止。
什么情况,这里没有用volatile修饰flag啊,也没用锁啊;
真的没用锁吗?println源码如下:
- public void println(boolean x) {
- synchronized (this) {
- print(x);
- newLine();
- }
- }
原来是锁住了this对象,即out属性的实例,所以我们在这个场景里用锁的形式保证变量内存及时可见甚至可以是下面这样:
- import java.util.concurrent.TimeUnit;
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- System.out.println();
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
甚至还可以这样:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- synchronized ("123"){
- }
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
但是不能这样:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- synchronized ("123"){
- }
- while (!flag){
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
正常用锁的方式
还是写点正常点的代码吧。。。也是最基础的例子
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!isFlag()){
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- setFlag(true);
- }
- public synchronized boolean isFlag() {
- return flag;
- }
- public synchronized void setFlag(boolean flag) {
- this.flag = flag;
- }
- }
在这个场景中,用锁的方式大同小异,不管是用wait-notifyAll,还是lock*,await-signallAll,亦或是,countdown,await,take,put等方法 ,都是在用锁而已。
对DCL单例模式的思考
在DCL单例中,既然锁synchronized能保证原子性和可见性,那volatile的作用是什么呢?volatile起的作用是禁止指令重排序和可见性。
- public class DoubleCheckedLocking {
- private volatile static DoubleCheckedLocking dcl = null;
- private DoubleCheckedLocking() {
- }
- public static DoubleCheckedLocking getInstance() {
- if (dcl == null) {// 第一个if不用获取锁就能判断对象是否为null(效率),第二个if存在的原因是线程安全
- synchronized (DoubleCheckedLocking.class) {
- if (dcl == null) {
- dcl = new DoubleCheckedLocking();
- }
- }
- }
- return dcl;
- }
- }
对于"dcl = new DoubleCheckedLocking();"这行代码,首先DoubleCheckedLocking.java被编译成字节码,然后被类加载器加载,接着还有下面3步骤:
memory = allocate(); // 1.分配内存空间
init(memory); // 2.将对象初始化
dcl = memory;// 3.设置dcl指向刚分配的内存地址,此时dcl != null
step2和step3在单线程环境下允许指令重排,即先把未初始化的内存地址指向dcl(此时dcl!=null),然后才把内存空间初始化;
但是如果在多线程的环境下,JVM优化指令重排后执行顺序如果是step1->step3->step2,A线程执行到step3此时还未执行step2对象还未初始化,但是此时dcl已经被赋值为memory,所以dcl!=null,同时另一个线程B执行最外层代码块if(dcl==null结果为false),就直接return未被初始化的错误的dcl。
从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用的更多相关文章
- 从原子类和Unsafe来理解Java内存模型,AtomicInteger的incrementAndGet方法源码介绍,valueOffset偏移量的理解
众所周知,i++分为三步: 1. 读取i的值 2. 计算i+1 3. 将计算出i+1赋给i 可以使用锁来保持操作的原子性和变量可见性,用volatile保持值的可见性和操作顺序性: 从一个小例子引发的 ...
- java连接mysql的一个小例子
想要用java 连接数据库,需要在classpath中加上jdbc的jar包路径 在eclipse中,Project的properties里面的java build path里面添加引用 连接成功的一 ...
- java操作xml的一个小例子
最近两天公司事比较多,这两天自己主要跟xml打交道,今天更一下用java操作xml的一个小例子. 原来自己操作xml一直用这个包:xstream-1.4.2.jar.然后用注解的方式,很方便,自己只要 ...
- 使用Trinity拼接以及分析差异表达一个小例子
使用Trinity拼接以及分析差异表达一个小例子 2017-06-12 09:42:47 293 0 0 Trinity 将测序数据分为许多独立的de Brujin grap ...
- MVVM模式的一个小例子
使用SilverLight.WPF也有很长时间了,但是知道Binding.Command的基本用法,对于原理性的东西,一直没有深究.如果让我自己建一个MVVM模式的项目,感觉还是无从下手,最近写了一个 ...
- Hutool :一个小而全的 Java 工具类库
Hutool 简介 Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以"甜甜的 ...
- Spring和Hibernate结合的一个小例子
1.新建一个SpringHibernate的maven项目 2.pom文件的依赖为 <dependency> <groupId>junit</groupId> &l ...
- Hadoop中RPC协议小例子报错java.lang.reflect.UndeclaredThrowableException解决方法
最近在学习传智播客吴超老师的Hadoop视频,里面他在讲解RPC通信原理的过程中给了一个RPC的小例子,但是自己编写的过程中遇到一个小错误,整理如下: log4j:WARN No appenders ...
- 一个Java内存可见性问题的分析
如果熟悉Java并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”: 在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值: 这里并非说的 ...
随机推荐
- python反转链表和成对反转
https://www.cnblogs.com/tianqizhi/p/9673894.html https://blog.csdn.net/weixin_34168700/article/detai ...
- 吴裕雄--天生自然KITTEN编程:翻译机
- 2.mac下 安装go-ethereum
Contents 上代码 A.前期准备:http://m.2cto.com/kf/201612/573010.html (1)安装python2.7,mac在终端中直接使用以下命令:brew inst ...
- 关于Markdown下无法使用表格的解决方案
关于Markdown下无法使用表格的解决方案 写表格,出现如下场景 解决方法.点击左下角M的表示,切换到extra模式 打开了新世界.如果不能点击,估计是你没有激活pro的权限,百度下就可以了. 或者 ...
- APScheduler使用总结
安装 pip install apscheduler APScheduler组件 1.triggers(触发器) 触发器中包含调度逻辑,每个作业都由自己的触发器来决定下次运行时间.除了他们自己初始配置 ...
- 聊一聊MyBatis 和 SQL 注入间的恩恩怨怨
整理了一些Java方面的架构.面试资料(微服务.集群.分布式.中间件等),有需要的小伙伴可以关注公众号[程序员内点事],无套路自行领取 更多优选 一口气说出 9种 分布式ID生成方式,面试官有点懵了 ...
- Git 相关问题分享,git reset与git revert的区别?
1.如果我在git add 后想要撤销操作,该怎么做? 使用 git rm --cache [文件名/ *] 或者 git reset HEAD, 为什么这个命令也会有效果呢,实际上reset将 HE ...
- 伪元素 before 和 after 初探
伪元素 before 和 after 初探 使用了 CodePen 做演示,欢迎点击预览 定义 首先来看 MDN 的定义: ::before 创建一个伪元素,作为已选中元素的第一个子元素,常通过 co ...
- 探索 Redux4.0 版本迭代 论基础谈展望(对比 React context)
Redux 在几天前(2018.04.18)发布了新版本,6 commits 被合入 master.从诞生起,到如今 4.0 版本,Redux 保持了使用层面的平滑过渡.同时前不久, React 也从 ...
- Python -Selenium的安装和调用
安装selenium步骤: 1.安装pip(cmd命令行管理员方式): pip install pip 也可直接搜索pip,到官网下载安装 2.安装selenium(cmd命令行管理员方式): pip ...