根据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内存模型
一.什么是内存模型,为什么要使用它 如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果 编译器把变量保存在本地寄存器而不是内存中 编译器中生成的指令顺序,可以与源代码中的顺 ...
随机推荐
- java 输入输出IO流:标准输入/输出System.in;System.out;System.err;【重定向输入System.setIn(FileinputStream);输出System.setOut(printStream);】
Java的标准输入输出分别通过System.in和System.out来代表的,在默认情况下它分别代表键盘和显示器,当程序通过System.in来获取输入时,实际上是从键盘读取输入 当程序试图通过 S ...
- 『与善仁』Appium基础 — 30、操作微信小程序
目录 1.测试微信小程序前提 2.获取微信小程序的进程 3.代码示例 4.补充:(了解) 微信小程序和微信公众号的测试方式基本上是一样的. 微信的小程序越来越多了,随之带来的问题是:小程序如何做自动化 ...
- 【LeetCode】1064. Fixed Point 解题报告(C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 暴力求解 日期 题目地址:https://leetco ...
- 【LeetCode】1019. Next Greater Node In Linked List 解题报告 (Python&C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 单调递减栈 日期 题目地址:https://leetc ...
- 【LeetCode】200. Number of Islands 岛屿数量
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 DFS BFS 日期 题目地址:https://le ...
- 初识TMMi——测试成熟度模型集成
利用零碎的时间,粗略了解了一下TMMi V1.2,整理一下学习笔记跟大家分享一下. 本文分为四个部分,分别为TMMi概述.TMMi结构.成熟度级别和过程域.TMMi实施周期,希望能够帮助大家更好的理解 ...
- 人脸搜索项目开源了:人脸识别(M:N)-Java版
一.人脸检测相关概念 人脸检测(Face Detection)是检测出图像中人脸所在位置的一项技术,是人脸智能分析应用的核心组成部分,也是最基础的部分.人脸检测方法现在多种多样,常用的技术或工具大 ...
- [开发配置]Linux系统配置开发环境
deeplin系统配置开发环境 开发系统:deeplin 15.11 开发工具:Clion 2019.2 ; PyCharm 2019 ; Idea 2019; Android Studio 开源库 ...
- Android物联网应用程序开发(智慧园区)—— 图片预览界面
效果图: 实现步骤: 1.首先在 build.gradle 文件中引入 RecycleView implementation 'com.android.support:recyclerview-v7: ...
- 使用 DML语句针对仓库管理信息系统,进行查询操作
查看本章节 查看作业目录 需求说明: 查询所有电视机产品的基本信息,要求显示产品编号.产品名和进货单价 查询所有产品的基本信息,要求按类型升序.价格降序显示查询信息 显示所有不重复的产品类型 显示进货 ...