在文章的开始,我们先来看一段代码以及他的执行情况:

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原则的定义如下:

  1. 如果一个操作 happens-before 于另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在 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法则借助同步的更多相关文章

  1. PR视屏剪切

    一款常用的视频编辑软件,由Adobe公司推出.现在常用的有CS4.CS5.CS6.CC.CC 2014及CC 2015版本.是一款编辑画面质量比较好的软件,有较好的兼容性,且可以与Adobe公司推出的 ...

  2. 《Java并发编程实战》第十六章 Java内存模型 读书笔记

    Java内存模型是保障多线程安全的根基,这里不过认识型的理解总结并未深入研究. 一.什么是内存模型,为什么须要它 Java内存模型(Java Memory Model)并发相关的安全公布,同步策略的规 ...

  3. final域的内存语义

    final 一.final的基本语义 final关键字可以用来修饰类.方法和变量(包括成员变量和局部变量) 当用final修饰一个类时,表明这个类不能被继承. 当用final修饰一个方法时,表明这个方 ...

  4. Unity3D之IOS&Android收集Log文件

    开发项目的时候尤其在处理与服务器交互这块,如果服务端程序看不到客户端请求的Log信息,那么无法修改BUG.在Windows上Unity会自动讲Log文件写入本地,但是在IOS和Android上确没有这 ...

  5. 《java并发编程实战》

    目录 对本书的赞誉 译者序 前 言 第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力 1.2.2 建模的简单性 1.2.3 异步事件的简化处理 1.2.4 响应更 ...

  6. (转)Unity3D研究院之IOS&Android收集Log文件

    转自:http://www.xuanyusong.com/archives/2477 有段时间没有写过文章了,不知道大伙儿还记得雨松MOMO吗? 嘿嘿. 开发项目的时候尤其在处理与服务器交互这块,如果 ...

  7. java并发实践笔记

    底层的并发功能与并发语义不存在一一对应的关系.同步和条件等底层机制在实现应用层协议与策略须始终保持一致.(需要设计级别策略.----底层机制与设计级策略不一致问题). 简介 1.并发简史.(资源利用率 ...

  8. vCenter 6.5安装

    http://guanjianfeng.com/archives/1160269 最近,VMware发布了vSphere 6.5版本,之前的最新版本为6.0.新版本已经开始试行使用HTML5来管理vS ...

  9. java并发编程实战:第十六章----Java内存模型

    一.什么是内存模型,为什么要使用它 如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果 编译器把变量保存在本地寄存器而不是内存中 编译器中生成的指令顺序,可以与源代码中的顺 ...

随机推荐

  1. java 数据类型:集合接口Collection之List~ArrayList:remove移除;replaceAll改变原有值;sort排序;迭代器listIterator();

    什么是List集合: 特点: 元素有序可重复的集合. 集合中每个元素都有其对应的顺序索引. List集合默认按元素的添加顺序设置元素的索引,索引从0开始.   List接口的常用方法: List可以使 ...

  2. netty系列之:小白福利!手把手教你做一个简单的代理服务器

    目录 简介 代理和反向代理 netty实现代理的原理 实战 总结 简介 爱因斯坦说过:所有的伟大,都产生于简单的细节中.netty为我们提供了如此强大的eventloop.channel通过对这些简单 ...

  3. [源码解析] PyTorch 分布式之弹性训练(2)---启动&单节点流程

    [源码解析] PyTorch 分布式之弹性训练(2)---启动&单节点流程 目录 [源码解析] PyTorch 分布式之弹性训练(2)---启动&单节点流程 0x00 摘要 0x01 ...

  4. 【LeetCode】341. Flatten Nested List Iterator 解题报告(Python&C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 递归+队列 栈 日期 题目地址:https://lee ...

  5. 使用zTree插件实现可拖拽的树

    在目前接触到的树插件中,我觉得zTree比较简单,也容易上手.有一次业务需求是将某对象分组树上的对象可以随意拖拽,相当于改变了对象的分组,因此我用到了zTree,对其进行了一些列学习.         ...

  6. Feign动态调用,结合Ribbon

    代码如下,三种方法: import org.springframework.beans.factory.annotation.Autowired;import org.springframework. ...

  7. [c++]对vector<T>容器求交集,并集,去重

    #include "iostream" #include "vector" #include "algorithm" //sort函数.交并 ...

  8. BUUCTF [极客大挑战 2019]Not Bad

    总的来说这是一个64位orw的题 开头先在主函数里分配了一个很大的空间 1 __int64 __fastcall main(int a1, char **a2, char **a3) 2 { 3 mm ...

  9. gojs 实用高级用法

    大家,新年好! 历史文章: 数据可视化 gojs 简单使用介绍 gojs 如何实现虚线(蚂蚁线)动画? 本文介绍的是在使用 gojs 制作图的过程中,你可能会碰到的问题的一些解决方案. gojs 是一 ...

  10. 【C#】C#中使用GDAL3(三):Windows下编译插件驱动

    转载请注明原文地址:https://www.cnblogs.com/litou/p/15720236.html 本文为<C#中使用GDAL3>的第三篇,总目录地址:https://www. ...