最早读《深入理解java虚拟机》对于volatile部分就没有读明白,最近重新拿来研究并记录一些理解

理解volatile前需要把以下这些概念或内容理解:

1、JMM内存模型

2、并发编程的三问题:原子性、一致性、有序性

3、先行发生原则

然后我们结合上面的几个知识点来看volatile如何使用

JMM内存模型

先看一下上面这张图片,即Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存

那么JMM为何要如此设计?其主要原因有两点:1、达到各平台访问内存效果的一致性 2、提升数据访问速度

对于提升数据访问速度,主要用到了CPU高速缓存这部分内容:计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存

在本文中,JMM能够帮助我们理解为什么会发生可见性问题

并发编程的三问题:原子性、可见性、有序性

原子性问题

原子性指:一个操作执行时不能被打断或插入

比如i++,JVM指令包括3个操作:读取x的值,进行加1操作,写入新的值,如果并发执行i++,可能这三步操作不同线程会穿插执行,原子性就是指,任何一个线程运行这三个操作时,其他线程不能进入运行这三步操作

如何解决原子性问题:
1、synchronized 2、Lock、其他锁

可见性问题

每个线程都有各自的工作内存(高速缓存、详见JMM),线程A更改了变量的值后,线程B从自己的工作内存中获取变量的值还可能是A修改前的值

如何解决可见性问题:

1、volatile关键字 2、Lock、synchronized

有序性问题

先看下什么是指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。如果程序不满足先行发生原则,那么可能发生指令重排

指令重排就影响了程序的有序性

如何解决有序性问题
1、volatile关键字 2、Lock、synchronized

从上面的三个问题来看volatile只能解决:可见性问题、有序性问题,但无法解决原子性问题,原子性问题仍需要锁的手段才能解决

先行发生原则 Happens-Before

先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据,先行发生原则,可以帮你判定是否并发安全的,从而不必去猜测是否是线程安全了

下面是Java内存模型中一些“天然的”先行发生关系,这些先行发生关系无需任何同步协助器协作java自带这些规则,可以直接在编码中使用。如果两个关系不在此列,而又无法通过这些关系推导出来,它们的顺序就无法保证,虚拟机可以对它们任意重排序

程序次序规则: 同一个线程内,按照代码出现的顺序,前面的代码 happens-before 后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
管程锁定规则: 对于一个监视器锁的unLock操作 happens-before 于每个后续对同一监视器锁的Lock操作。
volatile变量规则: 对volatile域的写入操作 happens-before 于每个后续对同一个域的读操作。
线程启动规则: 在同一个线程里,对Thread.start的调用会 happens-before 于每一个启动线程中的动作。
线程终结规则: 线程中的所有动作都 happens-before 于其它线程检测到这个线程已经终结,或者从Thread.join()调用成功返回,或者Thread.isAlive返回false.
中断规则: 一个线程调用另一个线程的interrupt happens-before 于被中断的线程发现中断(通过抛出InterruptedException 或者调用isInterrupted和interrupted)
终结规则: 一个对象的构造函数的结束 happens-before 于这个对象finalizer的开始
传递性: 如果 A happens-before 于 B,且 B happens-before 于 C,则 A happens-before 于C。

其中比较重要且难以理解的几条是:

程序次序规则

一段程序代码的执行在单个线程中看起来是有序的。虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性

管程锁定规则

一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作

volatile变量规则

对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作,如果线程1写入了volatile变量v,接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程),可以看成是volatile解决可见性问题的描述

总结下来就是先行发生原则可以确定两件事:

1、能帮助我们判断程序是否线程安全

2、能帮助我们确定程序是否可能发生指令重排

使用volatile

有了以上知识储备我们来看一下volatile如何正确的使用

1、多读单写

只有一个线程控制改变volitile变量的值,一个或多个线程并发读取volitile变量的值都可以用volitile

通常:线程开关或者状态标记的场景可以使用

因为可见性保证了volatile多读单写的能力,但又因为volatile没有解决原子性问题的能力,所以不是多读多写

public static volatile boolean flag = false;
//这种情况不添加volatile就有可能造成无法退出程序了
//添加了volatile就强制从主内存中获得值,就不会出现上述问题了
//这个例子体现:只有一个线程控制改变volatile变量的值 很多线程并发读取volitile变量的值都可以用volatile
new Thread(() -> {
while(!flag){
}
System.out.println("退出了");
}).start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("setup");
flag = true;
//特别说明:我测试flag是非volatile,当不在while(!flag){上sleep,会一直循环,这种非常可能拿不到更改后的值,一直从工作内容中获得缓存值false。

2、防止指令重排

防止指令重排,通常:单例懒汉模式 double-check中使用

public class LazySingleton {
private volatile static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
synchronized (LazySingletonill.class){
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
public static void main(String[] args){
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance().hashCode());
}).start();
}
}
}

我们来看一下为什么不加volatile会引发指令重排的问题:

首先,这个出现问题的概率并不高,并且我通过jdk8的版本反编译并未和帖子内容一致,姑且先把帖子的原理写一下:

instance = new LazySingleton();,其实JVM内部已经转换为多条指令:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

但是经过指令重排序后如下:

memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); //2:初始化对象

2、3步骤指令重排后发生了交换

假如线程A获得了锁并且正在执行lazySingleton = new LazySingleton();,这个实例化的jvm指令发生了重排,即instance = memory先于ctorInstance(memory)执行,刚好instance = memory执行完毕,线程B登场在执行if(lazySingleton == null){时为false,线程B return了一个没有初始化对象的实例出去,出现了返回不正确结果的现象

 
发个牢骚:这个单例写法真的太矫情了,另外这种懒汉模式double-check写法演化问题分析详见:
https://gitee.com/zxporz/zxp-thread-test/blob/master/src/main/java/org/zxp/thread/volatileTest/singleton/LazySingletonill.java
 

volatile引发的一系列血案的更多相关文章

  1. DataSet筛选数据然后添加到新的DataSet中引发的一系列血案

    直入代码: var ds2 = new DataSet(); ) { ].Select(" usertype <> 'UU'"); ) { DataTable tmp ...

  2. 在centos服务器上配置gitlab钩子引发的一系列问题

    为了给公司的服务器上搭建gitlab环境并且配置钩子(实现在本地git push之后服务器自动git pull),整了好久,最后终于把问题解决了,下面是记录安装gitlab之后引发的一系列问题: 首先 ...

  3. iOS回顾笔记( 02 ) -- 由九宫格布局引发的一系列“惨案”

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  4. Dynamics CRM中一个查找字段引发的【血案】

    摘要: 本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复267或者20180311可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyon ...

  5. Feign 400错误引发的一系列问题

    Feign 400错误引发的一系列问题 问题介绍 在使用Feign进行远程调用的时候出现非常奇怪的400错误,错误信息大概如下: feign.FeignException: status 400 re ...

  6. 记一次全站升级https引发的一系列问题

    中秋假期,闲来无事.花了一下午折腾了下https,说实话这年头还有网站不上https显然是折腾精神不够啊~ 1.SSL证书评估 看了市面上各种类型的证书,有收费的也有免费的,但是最终还是选择了腾讯云提 ...

  7. break使用不当引发的一个“血案”

    最近在网上冲浪,读到一则新闻,摘抄下这则新闻: ======================= 以下文字摘抄自互联网==================== 1990年1月15日,AT&T电话 ...

  8. 未关闭虚拟机直接关闭vmware引发的一系列问题——Windows下linux虚拟机

    虚拟机长时间挂起重新打开时卡顿,无法开启,脑抽直接关闭了vmware软件引起的一系列问题. 原因是关闭了vmware,但是相应的虚拟机并没有关闭,所以虚拟机不能重开 会出现如下提示 解决方案如下: 1 ...

  9. 配置进程外Session 同时解决一个奇怪的BUG 因为SQLserver 服务器名不是默认的.或者localhost而引发的一系列问题

    用公司的电脑学习如鹏网的视频,开发一个项目,用到了进程外session,因为公司电脑SQLServer 是2008 服务器名称是.  然后参考这篇文章进行设置进程外session 很顺利 完成了设置. ...

随机推荐

  1. 我对微信小程序的一些认识

    一. 什么是微信小程序. 微信小程序是指微信公众平台小程序,小程序可以帮助开发者快速的开发小程序,小程序可以在微信内被便捷地获取和传播:是不需要下载和安装既可以使用的应用小程序,是和原有的三种微信公众 ...

  2. Flask RESTful API搭建笔记

    之前半年时间,来到项目的时候,已经有一些东西,大致就是IIS+MYSQL+PHP. 所以接着做,修修补补,Android/iOS与服务器数据库交换用PHP, Web那边则是JS+PHP,也没有前后端之 ...

  3. linux抢占式调度

    为什么会发生调度?   因为cpu是有限的,而操作系统上的进程很多,所以操作系统需要平衡各个进程的运行时间 比如说有的进程运行时间已经很长了,已经占用了cpu很长时间了,这个时候操作系统要公平 就会换 ...

  4. Segment Tree Range Minimum Query.

    int rangeMinQuery(int segTree[], int qlow, int qhigh, int low, int high, int pos) { if (qlow <= l ...

  5. PHP RSA算法 HMAC-SHA1加密算法

    HMAC-SHA1加密算法 function getSignature($str, $key) { $signature = ""; if (function_exists('ha ...

  6. 动手写一个简单版的谷歌TPU

    谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1(后简称TPU)相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU ...

  7. Kali Linux 工具清单

    Kali Linux 工具清单 Information Gathering acccheck ace-voip Amap Automater bing-ip2hosts braa CaseFile C ...

  8. C语言实现折半插入算法

    #include <stdio.h> int BInsertSort(int array[],int left,int right){ //接收主函数调用语句中的实参传到这里的形参里 in ...

  9. [LOJ 2082] 「JSOI2016」炸弹攻击 2

    [LOJ 2082] 「JSOI2016」炸弹攻击 2 链接 链接 题解 枚举发射源,将发射源当做原点,对敌人和激光塔极角排序. 由于敌人纵坐标均为正,而其它点均为负,因此每两个角度差在 \(\pi\ ...

  10. [LOJ3054] 「HNOI2019」鱼

    [LOJ3054] 「HNOI2019」鱼 链接 链接 题解 首先想 \(O(n^3)\) 的暴力,不难发现枚举 \(A\) 和 \(D\) 后, \((B,C)\) 和 \((E,F)\) 两组点互 ...