环境

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的核心作用的更多相关文章

  1. 从原子类和Unsafe来理解Java内存模型,AtomicInteger的incrementAndGet方法源码介绍,valueOffset偏移量的理解

    众所周知,i++分为三步: 1. 读取i的值 2. 计算i+1 3. 将计算出i+1赋给i 可以使用锁来保持操作的原子性和变量可见性,用volatile保持值的可见性和操作顺序性: 从一个小例子引发的 ...

  2. java连接mysql的一个小例子

    想要用java 连接数据库,需要在classpath中加上jdbc的jar包路径 在eclipse中,Project的properties里面的java build path里面添加引用 连接成功的一 ...

  3. java操作xml的一个小例子

    最近两天公司事比较多,这两天自己主要跟xml打交道,今天更一下用java操作xml的一个小例子. 原来自己操作xml一直用这个包:xstream-1.4.2.jar.然后用注解的方式,很方便,自己只要 ...

  4. 使用Trinity拼接以及分析差异表达一个小例子

    使用Trinity拼接以及分析差异表达一个小例子  2017-06-12 09:42:47     293     0     0 Trinity 将测序数据分为许多独立的de Brujin grap ...

  5. MVVM模式的一个小例子

    使用SilverLight.WPF也有很长时间了,但是知道Binding.Command的基本用法,对于原理性的东西,一直没有深究.如果让我自己建一个MVVM模式的项目,感觉还是无从下手,最近写了一个 ...

  6. Hutool :一个小而全的 Java 工具类库

    Hutool 简介 Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以"甜甜的 ...

  7. Spring和Hibernate结合的一个小例子

    1.新建一个SpringHibernate的maven项目 2.pom文件的依赖为 <dependency> <groupId>junit</groupId> &l ...

  8. Hadoop中RPC协议小例子报错java.lang.reflect.UndeclaredThrowableException解决方法

    最近在学习传智播客吴超老师的Hadoop视频,里面他在讲解RPC通信原理的过程中给了一个RPC的小例子,但是自己编写的过程中遇到一个小错误,整理如下: log4j:WARN No appenders ...

  9. 一个Java内存可见性问题的分析

    如果熟悉Java并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”: 在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值: 这里并非说的 ...

随机推荐

  1. [洛谷P1495] 曹冲养猪 (中国剩余定理模板)

    中国剩余定理(朴素的)用来解线性同余方程组: x≡a[1] (mod m[1]) x≡a[2] (mod m[2]) ...... x≡a[n] (mod m[n]) 定义ms=m[1]*m[2]*. ...

  2. Tortoises SVN 教程

    1.  TortoiseSVN 简介 版本控制是管理信息修改的艺术,它一直是程序员最重要的工具,程序员经常会花时间作出小的修改,然后又在某一天取消了这些修改,想象一下一个开发者并行工作的团队 - 或许 ...

  3. MySQL5.7彻底取消主从复制

    由于手误在master节点执行了stop slave;->change master to XXX;->start slave;的动作,后面虽然使用stop slave停止了主从复制,但是 ...

  4. makefile(3)函数

    前言 学习make和makefile的主要目的是分析大型项目的源代码的关系,上一节我们讲述了makefile 中的变量,本节主要学习一下 makefile 中的函数,首先函数肯定可以分为几部分: 内置 ...

  5. Autotestplat体验中心

    web端 移动端 可戳[阅读原文]进行体验

  6. 我的学习归纳方法(以学习Maven为例)

    以我的个人角度来看待学习这件长久的事,希望对你有帮助,也希望你能提一下你的意见 本文初衷 把自己模板化 以此篇为引,与同行沟通心得,所以在此严重要求如果你有对应的心得还请能回复下,真心感谢!(鞠躬) ...

  7. python3.4多线程实现同步的四种方式

    临界资源即那些一次只能被一个线程访问的资源,典型例子就是打印机,它一次只能被一个程序用来执行打印功能,因为不能多个线程同时操作,而访问这部分资源的代码通常称之为临界区. 1. 锁机制 threadin ...

  8. python爬虫-smtplib模块发送邮件

    1.代码如下: import smtplib from email.message from EmailMessage # smtplib模块负责发送邮件服务 # email.message模块负责构 ...

  9. 一次js自定义播放器,canvas绘制弹幕的尝试

    不多bb,就直接说实现了什么功能: 1. 视频播放进度调整 2. 视频小窗口实时预览 3. 声音调整 4. 画中画模式 5. 网页全屏 6. 视频全屏 7. canvas绘制弹幕 8. 选中弹幕悬停 ...

  10. 查询mysql版本号

    mysql> select version(); +------------+| version() |+------------+| 5.7.23-log |+------------+1 r ...