从一个小例子引发的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并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”: 在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值: 这里并非说的 ...
随机推荐
- Android开发之《libyuv库的使用》
转自:http://bashell.nodemedia.cn/archives/build-google-libyuv-with-ndk-for-android.html 官方网站:https://c ...
- jQuery的html(),text()和val()比较
.html()用为读取和修改元素的HTML标签: .text()用来读取或修改元素的纯文本内容: .val()用来读取或修改表单元素的value值: 一看黑体的部分,所以把text和html分为一组, ...
- python有关汉字编码问题
python分为:程序编码(python安装程序).文件编码. 查看程序编码方式:sys.getdefaultencoding() 查看文件编码方式:1.import chardet 2. f = ...
- MIUI 7 会是小米的救命稻草吗?
7 会是小米的救命稻草吗?" title="MIUI 7 会是小米的救命稻草吗?"> 花无百日红,人无千日好.再绚烂的曾经,或许一朝不慎,就会成为过去.在科技圈,诺 ...
- javascript中的function命名空間與模擬getter、setter
function的命名空間 在javascript中,function也可以擁有自己的命名空間例如以下這段程式碼: 12345678 function () { return 'I am A';} A ...
- 压力测试(七)-html可视化压测报告细讲
1.阿里云Linux服务器 Jmeter压测实战之jtl文件生成和查看 简介: 利用软件从阿里云Centos服务器下载压测报告,讲解Jtl文件,并怎么查看文件 可以通过打开jmeter,新建线程组-& ...
- 手把手教你如何用MSF进行后渗透测试!
在对目标进行渗透测试的时候,通常情况下,我们首先获得的是一台web服务器的webshell或者反弹shell,如果权限比较低,则需要进行权限提升:后续需要对系统进行全面的分析,搞清楚系统的用途:如果目 ...
- JSR310-新日期APIJSR310新日期API(完结篇)-生产实战
前提 前面通过五篇文章基本介绍完JSR-310常用的日期时间API以及一些工具类,这篇博文主要说说笔者在生产实战中使用JSR-310日期时间API的一些经验. 系列文章: JSR310新日期API(一 ...
- Go语言基础篇(1) —— 编写第一个Go程序
创建文件hello_world.go package main //包,表名代码所在的包 import "fmt" //引入依赖 //main方法 func main(){ fmt ...
- python大佬养成计划----HTML网页设计(序列)
序列化标签 1.有序标签--ol和li 有序列表标签是<ol>,是一个双标签.在每一个列表项目前要使用<li>标签.<ol>标签的形式是带有前后顺序之分的编号.如果 ...