引言

上一篇文章聊到了Java内存模型,在其中我们说JMM是建立在happens-before(先行发生)原则之上的。

为什么这么说呢?因为在Java程序的执行过程中,编译器和处理器对我们所写的代码进行了一系列的优化来提高程序的执行效率。这其中就包括对指令的“重排序”。

重排序导致了我们代码并不会按照代码编写顺序来执行,那为什么我们在程序执行后结果没有发生错乱,原因就是Java内存模型遵循happens-before原则。在happens-before规则下,不管程序怎么重排序,执行结果不会发生变化,所以我们不会看到程序结果错乱。

重排序

重排序是什么?通俗点说就是编译器和处理器为了优化程序执行性能对指令的执行顺序做了一定修改。

重排序会发生在程序执行的各个阶段,包括编译器冲排序、指令级并行冲排序和内存系统重排序。这里不具体分析每个重排序的过程,只要知道重排序导致我们的代码并不会按照我们编写的顺序来执行。

在单线程的的执行过程中发生重排序后我们是无法感知的,如下代码所示,

int a = 1;  //步骤1
int b = 2; //步骤2
int c = a + b; //步骤3

1和2做了重排序并不会影响程序的执行结果,在某些情况下为了优化性能可能会对1和2做重排序。2和3的重排序会影响执行结果,所以编译器和处理器不会对2和3进行重排序。

在多线程中如果没有进行正确的同步,发生重排序我们是可以感知的,比如下面的代码:

public class AAndB {

	int x = 0;
int y = 0;
int a = 0;
int b = 0; public void awrite() { a = 1;
x = b;
} public void bwrite() { b = 1;
y = a;
}
} public class AThread extends Thread{ private AAndB aAndB; public AThread(AAndB aAndB) { this.aAndB = aAndB;
} @Override
public void run() {
super.run(); this.aAndB.awrite();
}
} public class BThread extends Thread{ private AAndB aAndB; public BThread(AAndB aAndB) { this.aAndB = aAndB;
} @Override
public void run() {
super.run(); this.aAndB.bwrite();
}
} private static void testReSort() throws InterruptedException { AAndB aAndB = new AAndB(); for (int i = 0; i < 10000; i++) {
AThread aThread = new AThread(aAndB);
BThread bThread = new BThread(aAndB); aThread.start();
bThread.start(); aThread.join();
bThread.join(); if (aAndB.x == 0 && aAndB.y == 0) {
System.out.println("resort");
} aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0; } System.out.println("end");
}

如果不进行重排序,程序的执行顺序有四种可能:









但程序在执行多次后会打印出“resort”,这种情况就说明了A线程和B线程都出现了重排序。

happens-before的定义

happens-before定义了八条规则,这八条规则都是用来保证如果A happens-before B,那么A的执行结果对B可见且A的执行顺序排在B之前。

  1. 程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
  2. 管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
  3. volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
  4. 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
  5. 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
  8. 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

happens-before定义了这么多规则,其实总结起来可以归纳为一句话:happens-before规则保证了单线程和正确同步的多线程的执行结果不会被改变。

那为什么有程序次序规则的保证,上面多线程执行过程中还是出现了重排序呢?这是因为happens-before规则仅仅是java内存模型向程序员做出的保证。在单线程下,他并不关心程序的执行顺序,只保证单线程下程序的执行结果一定是正确的,java内存模型允许编译器和处理器在happens-before规则下对程序的执行做重排序。

而且从程序员角度来说,对于两个操作是否真的被重排序并不关心,关心的是程序执行结果是否被改变。

上面的程序在单线程会被重排序的情况下又没有对多线程同步,这样就导致了意料之外的结果。

as-if-serial语义

《Java并发编程的艺术》中解释:

as-if-serial就是不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

这句话通俗理解就是as-if-serial语义保证单线程程序的执行结果不会被改变。

本质上和happens-before规则是一个意思:happens-before规则保证了单线程和正确同步的多线程的执行结果不会被改变。都是对执行结果做保证,对执行过程不做保证。

这也是JMM设计上的一个亮点:既保证了程序员编程时的方便以及正确,又同时保证了编译器和处理器更大限度的优化自由。




参考资料:

《深入理解Java内存模型》

《深入理解Java虚拟机》

《Java并发编程的艺术》

Java并发(2)- 聊聊happens-before的更多相关文章

  1. Java并发(3)- 聊聊Volatile

    引言 谈到volatile关键字,大多数开发者都有一定了解,可以说是开发者非常熟悉,深入之后又非常陌生的一个关键字.相当于轻量的synchronized,也叫轻量级锁,与synchronized相比性 ...

  2. Java并发(10)- 简单聊聊JDK中的七大阻塞队列

    引言 JDK中除了上文提到的各种并发容器,还提供了丰富的阻塞队列.阻塞队列统一实现了BlockingQueue接口,BlockingQueue接口在java.util包Queue接口的基础上提供了pu ...

  3. Java并发(1)- 聊聊Java内存模型

    引言 在计算机系统的发展过程中,由于CPU的运算速度和计算机存储速度之间巨大的差距.为了解决CPU的运算速度和计算机存储速度之间巨大的差距,设计人员在CPU和计算机存储之间加入了高速缓存来做为他们之间 ...

  4. java并发编程资料

    并发这玩意很有用,把自己在网上看过觉得总结的很好的资料分享出来.猛击下面的地址查看吧 java并发编程:线程池的使用说明 java并发编程系列文章 Java并发性和多线程专题 并发工具类 Java 7 ...

  5. Java并发编程:并发容器之ConcurrentHashMap(转载)

    Java并发编程:并发容器之ConcurrentHashMap(转载) 下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concu ...

  6. Java并发编程:并发容器之ConcurrentHashMap

    转载: Java并发编程:并发容器之ConcurrentHashMap JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的 ...

  7. 《Java并发编程实战》第六章 任务运行 读书笔记

    一. 在线程中运行任务 无限制创建线程的不足 .线程生命周期的开销很高 .资源消耗 .稳定性 二.Executor框架 Executor基于生产者-消费者模式.提交任务的操作相当于生产者.运行任务的线 ...

  8. Java并发编程:并发容器ConcurrentHashMap

    Java并发编程:并发容器之ConcurrentHashMap(转载) 下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concu ...

  9. 【Java并发】详解 AbstractQueuedSynchronizer

    前言 队列同步器 AbstractQueuedSynchronizer(以下简称 AQS),是用来构建锁或者其他同步组件的基础框架.它使用一个 int 成员变量来表示同步状态,通过 CAS 操作对同步 ...

随机推荐

  1. Servlet异步处理和文件上传

    1. 什么是异步处理     原来,在服务器没有结束响应之前,浏览器是看不到响应内容的!只有响应结束时,浏览器才能显示结果!     现在异步处理的作用:在服务器开始响应后,浏览器就可以看到响应内容, ...

  2. 深入理解yii2之RBAC(模块化系统)

    一.前言 上一篇文章我们已经大致谈过RBAC到底是什么和yii2底层RBAC接口的分析. 下面我深入理解一下RBAC权限分配,深入理解下yii2底层RBAC扩展,以及它是如何针对模块化系统的开发的? ...

  3. source tree 使用心得

    SourceTree 是 Windows 和Mac OS X 下免费的 Git 和 Hg 客户端管理工具,同时也是Mercurial和Subversion版本控制系统工具.支持创建.克隆.提交.pus ...

  4. keil 使用C++编程主要要点

    1.中断处理,添加一下宏定义.如果不添加,中断服务函数不会链接到下载文件中:发生中断后,会停留在xxx.s文件的 "B ."语句. #ifdef __cplusplus exter ...

  5. 笔记-cookie参数

    笔记-cookie参数 cookie各个参数详解 定义和用法 setcookie()函数向客户端发送一个 HTTP cookie. cookie是由服务器发送到浏览器的变量.cookie 通常是服务器 ...

  6. Java 虚拟机结构分析

    本博文主要介绍了JVM(Java Virtual Machine)的组成部分以及它们内部的工作机制和原理.需要注意的是,虽然平时我们用的大多是Sun(现已被Oracle收购)JDK提供的JVM,但是J ...

  7. Python基础——安装运行

    Python是如何运行的? 像绝大多数编程语言一样,要在计算机上能够运行python程序,至少需要安装一个最小的Python包:一个Python解释器和支持的库. 安装Python 安装包下载:htt ...

  8. 站在C#和JS的角度细谈函数式编程与闭包

    1.函数式编程是什么? 摘自百度的说法是.函数式编程是种编程典范,它将电脑运算视为函数的计算.函数编程语言最重要的基础是 λ 演算(lambda calculus).而且λ演算的函数可以接受函数当作输 ...

  9. AS3项目基础框架搭建分享robotlegs2 + starling1.3 + feathers1.1

    这个框架和我之前使用robotlegs1版本的大体相同,今天要写一个新的聊天软件就把之前的框架升级到了2.0并且把代码整理了一下. 使用适配器模式使得starling的DisplayObject和fl ...

  10. 剑指Offer - 九度1349 - 数字在排序数组中出现的次数

    剑指Offer - 九度1349 - 数字在排序数组中出现的次数2013-11-23 00:47 题目描述: 统计一个数字在排序数组中出现的次数. 输入: 每个测试案例包括两行: 第一行有1个整数n, ...