从一个小例子引发的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并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”: 在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值: 这里并非说的 ...
随机推荐
- Zabbix-3.0.3实现微信(WeChat)报警
转自:http://blog.sina.com.cn/s/blog_87113ac20102w7hp.html Zabbix可以通过多种方式把告警信息发送到指定人,常用的有邮件,短信报警方式,但是越来 ...
- 吴裕雄--天生自然 PYTHON数据分析:钦奈水资源管理分析
df = pd.read_csv("F:\\kaggleDataSet\\chennai-water\\chennai_reservoir_levels.csv") df[&quo ...
- Pandas提取单元格的值
如提取第1行,第2列的值: df.iloc[[0],[1]] 则会返回一个df,即有字段名和行号. 如果用values属性取值: df.iloc[[0],[1]].values 返回的值会是列表,而且 ...
- scrollIntoView 前的元素滚动到浏览器窗口的可视区域内 不止垂直滚动,还有水平滚动
Element.scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域内 element.scrollIntoView(); // 等同于element.scrollIntoVi ...
- python pip配置以及安装工具包的一些方法
pip是python的一个工具包管理工具,可以下载安装需要的工具包,想要使用它来管理工具包首先要安装pip,安装方法可以参照下面这个网址来进行: https://www.cnblogs.com/Nan ...
- 关于线上bug
之所以想写下线上bug,因为发觉有些公司对线上bug的处理是比较严格甚至是很苛刻,涉及到的相关人可能会因此而背黑锅. 之所以会存在这样情况,因为公司各部门都有关联,特别是用户.老板的投诉,也给公司会造 ...
- Core Java之7种单例模式
初始化空 初始化创建 一把锁 两把锁 大专栏 Core Java之7种单例模式"headerlink" title="静态内部类">静态内部类 静态加载 ...
- C2C的道德边界:沦为从假运单到假病条的供假渠道
你可能刚开始学会不去看网购平台上商品回评中的虚假好评,却又要开始应对同事在朋友圈等平台买来的虚开病假条带来的困扰.最近各大媒体包括党报热传的网购病假条事件,再度将人们的目光集中在这个C2C模式之上.从 ...
- CentOS7搭建FTP Server
本文主要记录CentOS下FTP Server的安装和配置流程. 安装vsftpd yum install -y vsftpd 启动vsftpd service vsftpd start 运行下面的命 ...
- Particle Filter Algorithm
目录 问题提出 算法研究现状 算法原理 问题提出 在现实科研问题中,其中有很多都是非线性的.要想求得问题的解,就需要非线性的算法.所谓非线性滤波,就是基于带有噪声的观测值,估计非线性系统动态变化的状态 ...