volatile引发的一系列血案
最早读《深入理解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了一个没有初始化对象的实例出去,出现了返回不正确结果的现象
volatile引发的一系列血案的更多相关文章
- DataSet筛选数据然后添加到新的DataSet中引发的一系列血案
直入代码: var ds2 = new DataSet(); ) { ].Select(" usertype <> 'UU'"); ) { DataTable tmp ...
- 在centos服务器上配置gitlab钩子引发的一系列问题
为了给公司的服务器上搭建gitlab环境并且配置钩子(实现在本地git push之后服务器自动git pull),整了好久,最后终于把问题解决了,下面是记录安装gitlab之后引发的一系列问题: 首先 ...
- iOS回顾笔记( 02 ) -- 由九宫格布局引发的一系列“惨案”
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
- Dynamics CRM中一个查找字段引发的【血案】
摘要: 本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复267或者20180311可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyon ...
- Feign 400错误引发的一系列问题
Feign 400错误引发的一系列问题 问题介绍 在使用Feign进行远程调用的时候出现非常奇怪的400错误,错误信息大概如下: feign.FeignException: status 400 re ...
- 记一次全站升级https引发的一系列问题
中秋假期,闲来无事.花了一下午折腾了下https,说实话这年头还有网站不上https显然是折腾精神不够啊~ 1.SSL证书评估 看了市面上各种类型的证书,有收费的也有免费的,但是最终还是选择了腾讯云提 ...
- break使用不当引发的一个“血案”
最近在网上冲浪,读到一则新闻,摘抄下这则新闻: ======================= 以下文字摘抄自互联网==================== 1990年1月15日,AT&T电话 ...
- 未关闭虚拟机直接关闭vmware引发的一系列问题——Windows下linux虚拟机
虚拟机长时间挂起重新打开时卡顿,无法开启,脑抽直接关闭了vmware软件引起的一系列问题. 原因是关闭了vmware,但是相应的虚拟机并没有关闭,所以虚拟机不能重开 会出现如下提示 解决方案如下: 1 ...
- 配置进程外Session 同时解决一个奇怪的BUG 因为SQLserver 服务器名不是默认的.或者localhost而引发的一系列问题
用公司的电脑学习如鹏网的视频,开发一个项目,用到了进程外session,因为公司电脑SQLServer 是2008 服务器名称是. 然后参考这篇文章进行设置进程外session 很顺利 完成了设置. ...
随机推荐
- LeetCode: 669 Trim a Binary Search Tree(easy)
题目: Given a binary search tree and the lowest and highest boundaries as L and R, trim the tree so th ...
- 我们团队是如何落地DDD的(1)
最近发现文章老是被窃取,有些平台举报了还没有用.请识别我的id方丈的寺院. 摘要 DDD领域驱动设计,起源于2004年著名建模专家Eric Evans发表的他最具影响力的著名书籍:Domain-Dri ...
- 【异步编程】Part2:掌控SynchronizationContext避免deadlock
引言: 多线程编程/异步编程非常复杂,有很多概念和工具需要去学习,贴心的.NET提供Task线程包装类和await/async异步编程语法糖简化了异步编程方式. 相信很多开发者都看到如下异步编程实践原 ...
- Spark history server 遇到的一些问题
最近学习Spark,看了一个视频,里面有提到启动spark后,一般都会启动Spark History Server.视频里把 spark.history.fs.logDirectory 设置成了Had ...
- 51nod1153(dfs/单调队列)
题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1153 题意:中文题诶- 思路:一个比较简单的方法是dfs隐式 ...
- 初次学习DropWizard框架——解决maven打包时出现没有主清单属性的问题
笔者因为公司的项目需要,开始接触DropWizard框架,照着官网https://www.dropwizard.io/0.9.2/docs/getting-started.html撸了一遍. 工具为I ...
- Linux常用命令(补充)-grep
grep(global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正 ...
- Net Core下通过Proxy 模式
Net Core下通过Proxy 模式 NET Core下的WCF客户端也是开源的,这次发布.NET Core 2.0,同时也发布了 WCF for .NET Core 2.0.0, 本文介绍在.NE ...
- linux替换文件中的某个字符串的命令sed
sed -i 's/java-7-oracle/java-8-oracle/g' /etc/init.d/tomcat7 上面的命令是将tomcat7中的java-7-oracle替换为java-8- ...
- Angular2中实现基于TypeScript的对象合并方法:extend()
TypeScript里面没有现成的合并对象的方法,这里借鉴jQuery里的$.extend()方法.写了一个TypeScript的对象合并方法,使用方法和jQuery一样. 部分代码和jQuery代码 ...