环境

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. 吴裕雄--天生自然 R语言开发学习:使用ggplot2进行高级绘图(续二)

    #----------------------------------------------------------# # R in Action (2nd ed): Chapter 19 # # ...

  2. URL与URI与URN的区别与联系

    1.什么是URL? 统一资源定位符(或称统一资源定位器/定位地址.URL地址等[1],英语:Uniform Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址).如同在网 ...

  3. 主成分分析(PCA)模型概述

    数据降维 降维是对数据高维度特征的一种预处理方法.降维是将高维度的数据保留下最重要的一些特征,去除噪声和不重要的特征,从而实现提升数据处理速度的目的.在实际的生产和应用中,降维在一定信息损失范围内,可 ...

  4. 将配置好的虚拟机文件导入VMware

    第一步:打开VMware Workstation Pro 第二步:  选择文件,图示: 第三步:点击打开,选择配置好的虚拟机文件目录 点击打开就ok了,图示

  5. nginx图片过滤处理模块http_image_filter_module安装配置

    http_image_filter_module是nginx提供的集成图片处理模块,支持nginx-0.7.54以后的版本,在网站访问量不是很高磁盘有限不想生成多余的图片文件的前提下可,就可以用它实时 ...

  6. grpc调试工具

    grpcurl 和 grpcui 都是调试grpc的利器,前者用于命令行,类似curl工具:后者是以web的形式进行调试的,类似postman工具. 有了这两款工具,我们不用写任何客户端代码,也能方便 ...

  7. SpringBoot快速上手系列01:入门

    1.环境准备 1.1.Maven安装配置 Maven项目对象模型(POM),可以通过一小段描述信息来管理项目的构建,报告和文档的项目管理工具软件. 下载Maven可执行文件 cd /usr/local ...

  8. 【WPF学习】第五十一章 动画缓动

    线性动画的一个缺点是,它通常让人觉得很机械且不能够自然.相比而言,高级的用户界面具有模拟真实世界系统的动画效果.例如,可能使用具有触觉的下压按钮,当单击时按钮快速弹回,但是当没有进行操作时它们会慢慢地 ...

  9. VUE深入浅出(学习过程)

    VUE 2020年02月26日06:27:10 复习过Java8新特性之后开始学习VUE. 了解node了之后,来了解一下VUE.针对于学习VUE用什么开发工具这个问题上,我这里有vsCode和web ...

  10. 操作系统-IO管理和磁盘调度

    I/O设备 IO设备的类型 分为三类:人机交互类外部设备:打印机.显示器.鼠标.键盘等等.这类设备数据交换速度相对较慢,通常是以字节为单位进行数据交换的 存储设备:用于存储程序和数据的设备,如磁盘.磁 ...