根据happens-before法则借助同步
在文章的开始,我们先来看一段代码以及他的执行情况:
public class PossibleRecording{
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
y = b;
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
x = a;
}
});
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
System.out.println("( " + x + " , " + y + " )");
}
}
执行结果:
( 0 , 1 )
( 1 , 0 )
( 1 , 1 )
( 0 , 0 )
对于上面这一段及其简单的代码,可以很简单的想到程序是如何打印( 0 , 1 ) 或 ( 1 , 0 ) 或 ( 1 , 1 ) 的,线程One可以在线程Two开始前完成,线程Two也可以在线程One开始前完成,又或者他们可以交替完成。但是奇怪的是,程序竟然可以打印( 0 , 0 ),下图展示了一种打印(0 , 0)的可能(由于每个线程中的动作都没有依赖其他线程的数据流,因此这些动作可以乱序执行):

在执行程序时,为了提高性能,编译器和处理器会对指令做重排序,内存级的重排序会让程序的行为变得不可预期。而同步就抑制了编译器、运行时和硬件对存储操作的各种方式的重排序,否则这些重排序将会破坏JMM提供的可见性保证。JMM确保在不同的编译器和不同的处理器平台上,通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的可见性保证。
那么在正确使用同步、锁的情况下,线程One修改了变量a的值何时对线程Two可见呢?我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这个规则就是happens-before。从JDK 5开始,JMM就是用happens-before的概念来阐述多线程之间的内存可见性。
happens-before原则的定义如下:
- 如果一个操作 happens-before 于另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在 happens-before 的关系,并不意味着一定要按照happens-before原则制定的顺序来执行,如果重排序之后的执行结果于按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。
happens-before法则包括:
- 程序次序法则:线程中的每个动作A都 happens-before 于该线程中的每一个动作B,其中在程序中,所有的动作B都出现在动作A之后
- 监视器锁法则:对一个监视器锁的解锁 happens-before 于每一个后续对同一监视器锁的枷锁
- 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
当一个变量被多个线程读取,且至少被一个线程写入时,如果读写操作并未依照排序,就会产生数据竞争。一个正确同步的线程是没有数据竞争的程序。加锁、解锁、对volatile变量的读写、启动一个线程以及检测线程是否结束这样的操作均是同步动作。
FutureTask源码解读
接下来看看FutureTask中是如何巧妙运用happens-before法则的。

在FutureTask中最重要的变量就是上图中标记出来的两个。
- state:是一个volatile修饰的变量,用于表示当前task的状态
- outcome:用于get()返回的正常结果,也可能是异常
注意看outcome后面的注释,在jdk源码中很少有这样的注释,一旦有这样的注释,那肯定是非常重要的。
理论上讲,outcome会被多个线程访问,其中应该是一个线程可以读写,其他的线程都只能读。那这种情况下,为啥不加上volatile呢?加上volatile的好处就是可以让outcome和state变量被修改后,其他线程可以立刻感知到。但作者为啥不加上volatile呢?
在整个类中,与outcome变量的写入操作,只有这两个地方:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
与outcome有关的读取操作,即get操作:
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}
接下来我们把目光集中到这三个方法上:set(),get(),report()
我们把get()和report()合并到一起,将多余的代码去掉,如下:
public V get() {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
Object x = outcome;
if (s == NORMAL);
return (V)x;
}
从上面可以看出,当state为NORMAL的时候,返回outcome。
再来看看set()方法:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
第二行,通过UNSAFE的cas操作将状态从NEW状态改为COMPLETING,cas设置成功之后,进入if方法里面,然后给outcome设置值,第四行,将state的状态设置为NORMAL状态,从备注中可以看到这是一个最终状态。那从NEW状态到NORMAL状态,中间有一个稍纵即逝的状态-COMPLETING。从get方法中可以看到,如果state的状态小于等于COMPLETING(即为NEW状态)时,就是当前线程没有抢到CPU的执行时间,进入等到状态。
我们把get()和set()的伪代码放在一起:

首先你读到标号为4的地方,读到的值是NORMAL,那么说明标号为3的地方一定已经执行过了,因为state是volatile修饰过的,根据happens-before关系:volatile变量法则:对 volatile 域的写入操作 happens-before 于每一个后续对同一个域的读操作。所以我们可以得出标号3的代码先于标号4的代码执行。
而又根据程序次序规则,即:
在一个线程内,按照控制流顺序,书写在前面的操作先行于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
可以得出:2 happens-before 3 happens-before 4 happens-before 5;
又根据传递性的规则,即:
传递性: 如果A happens-before 于B,且B happens-before 于C,则A happens-before 于 C
可以得出,2 happens-before 5。而2就是对outcome变量的写入,5是对outcome变量的读取。所以,虽然outcome的变量没有加volatile,但是他是通过被volatile修饰的state变量,借助了变量的happens-before关系,完成了同步的操作(即写入先于读取)。
参考文章:
推荐一个公众号:【why技术】 https://mp.weixin.qq.com/s/1SjOChRD0a241UCsBEAfCA
https://www.cmsblogs.com/?p=2102
https://blog.csdn.net/xixi_haha123/article/details/81155796
根据happens-before法则借助同步的更多相关文章
- PR视屏剪切
一款常用的视频编辑软件,由Adobe公司推出.现在常用的有CS4.CS5.CS6.CC.CC 2014及CC 2015版本.是一款编辑画面质量比较好的软件,有较好的兼容性,且可以与Adobe公司推出的 ...
- 《Java并发编程实战》第十六章 Java内存模型 读书笔记
Java内存模型是保障多线程安全的根基,这里不过认识型的理解总结并未深入研究. 一.什么是内存模型,为什么须要它 Java内存模型(Java Memory Model)并发相关的安全公布,同步策略的规 ...
- final域的内存语义
final 一.final的基本语义 final关键字可以用来修饰类.方法和变量(包括成员变量和局部变量) 当用final修饰一个类时,表明这个类不能被继承. 当用final修饰一个方法时,表明这个方 ...
- Unity3D之IOS&Android收集Log文件
开发项目的时候尤其在处理与服务器交互这块,如果服务端程序看不到客户端请求的Log信息,那么无法修改BUG.在Windows上Unity会自动讲Log文件写入本地,但是在IOS和Android上确没有这 ...
- 《java并发编程实战》
目录 对本书的赞誉 译者序 前 言 第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力 1.2.2 建模的简单性 1.2.3 异步事件的简化处理 1.2.4 响应更 ...
- (转)Unity3D研究院之IOS&Android收集Log文件
转自:http://www.xuanyusong.com/archives/2477 有段时间没有写过文章了,不知道大伙儿还记得雨松MOMO吗? 嘿嘿. 开发项目的时候尤其在处理与服务器交互这块,如果 ...
- java并发实践笔记
底层的并发功能与并发语义不存在一一对应的关系.同步和条件等底层机制在实现应用层协议与策略须始终保持一致.(需要设计级别策略.----底层机制与设计级策略不一致问题). 简介 1.并发简史.(资源利用率 ...
- vCenter 6.5安装
http://guanjianfeng.com/archives/1160269 最近,VMware发布了vSphere 6.5版本,之前的最新版本为6.0.新版本已经开始试行使用HTML5来管理vS ...
- java并发编程实战:第十六章----Java内存模型
一.什么是内存模型,为什么要使用它 如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果 编译器把变量保存在本地寄存器而不是内存中 编译器中生成的指令顺序,可以与源代码中的顺 ...
随机推荐
- xfs文件系统修复方法https://blog.csdn.net/yuanfang_way/article/details/78700089
首先尝试mount和umount文件系统,以便重放日志,修复文件系统,如果不行,再进行如下操作. 1.检查文件系统:先确保umount xfs_check /dev/sdd(盘符); echo $? ...
- javascript中的12种循环遍历方法
1.for (自定义条件) 循环 let arr = [1,2,3]; for(let i =0;i<arr.length;i++){ console.log(i,arr[i]) } 2.for ...
- Pointcut 表达式
AOP 概念篇 今天介绍 Pointcut 的表达式 通配符 常见的通配符如下 .. 含义一:方法表达式中.代表任意数量的参数 @Service public class HelloService { ...
- 使用unc0ver(5.3.1)执行jailbreak踩坑
官方网站: https://unc0ver.dev/ 目前版本: 5.3.1 使用mac os+altstore步骤: AltStore Download AltStore. Use the link ...
- 【LeetCode】1085. Sum of Digits in the Minimum Number 解题报告(C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历 日期 题目地址:https://leetcode ...
- CRB and His Birthday(hdu 5410)
CRB and His Birthday Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Oth ...
- Centos 切换中文输入法
切换输入法看起来是一个非常简单的操作,但是对于初学者来说,也并非那么简单,开始会发现按Ctrl+space无法切换中文输入法,原因是系统没有安装中文输入法,运行以下命令可以安装中文输入法: yum i ...
- Noise
目录 motivation settings results motivation 提高网络的一个有用的技巧就是增加训练数据: 真实数据, 或者用GAN拟合的数据. 这里想要研究的是, 噪声是否能够算 ...
- Jmeter环境变量配置你不得不知道的事情
在安装Jmeter的过程中大家肯定需要配置环境,但是为什么要配置JDK的环境变量呢?大家有没有好奇过,有没有仔细去像一下呢,其实在安装Jmeter前,大家应该都知道Jmeter是我们JAVA开发的,J ...
- vue基于Blob.js和 Export2Excel.js做前端导出
1安装三个依赖包 npm install -S file-saver@2.0.2 npm install -S xlsx@0.15.6 npm install -D script-loader@0.7 ...