《Java并发编程的艺术》一一3.3 顺序一致性

3.3 顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。
3.3.1 数据竞争与顺序一致性
当程序未正确同步时,就可能会存在数据竞争。Java内存模型规范对数据竞争的定义
如下。
在一个线程中写一个变量,
在另一个线程读同一个变量,
而且写和读没有通过同步来排序。
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(前一章的示例正是如此)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。
JMM对正确同步的多线程程序的内存一致性做了如下保证。
如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。马上我们就会看到,这对于程序员来说是一个极强的保证。这里的同步是指广义上的同步,包括对常用同步原语(synchronized、volatile和f?inal)的正确使用。
3.3.2 顺序一致性内存模型
顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性。
1)一个线程中的所有操作必须按照程序的顺序来执行。
2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
顺序一致性内存模型为程序员提供的视图如图3-10所示。

图3-10 顺序一致性内存模型的视图
在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作。从上面的示意图可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化(即在顺序一致性模型中,所有操作之间具有全序关系)。
为了更好进行理解,下面通过两个示意图来对顺序一致性模型的特性做进一步的说明。
假设有两个线程A和B并发执行。其中A线程有3个操作,它们在程序中的顺序是:A1→A2→A3。B线程也有3个操作,它们在程序中的顺序是:B1→B2→B3。
假设这两个线程使用监视器锁来正确同步:A线程的3个操作执行后释放监视器锁,随后B线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果将如图3-11所示。

图3-11 顺序一致性模型的一种执行效果
现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图,如图3-12所示。

图3-12 顺序一致性模型中的另一种执行效果
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1→A1→A2→B2→A3→B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其他线程看到的操作执行顺序将不一致。
3.3.3 同步程序的顺序一致性效果
下面,对前面的示例程序ReorderExample用锁来同步,看看正确同步的程序如何具有顺序一致性。
请看下面的示例代码。

class SynchronizedExample {
    int a = 0;
    boolean f?lag = false;

    public synchronized void writer() {        // 获取锁
        a = 1;
        f?lag = true;
    }                        // 释放锁

    public synchronized void reader() {        // 获取锁
        if (f?lag) {
            int i = a;
            ……
        }                        // 释放锁
    }
}

在上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图,如图3-13
所示。
顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图(具体细节后文会说明)。虽然线程A在临界区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行
结果。

图3-13 两个内存模型中的执行时序对比图
从这里我们可以看到,JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。
3.3.4 未同步程序的执行特性
对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,Null,False),JMM保证线程读操作读取到的值不会无中生有(Out Of Thin Air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会对内存空间进行清零,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在已清零的内存空间(Pre-zeroed Memory)分配对象时,域的默认初始化已经完成了。
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响。而且未同步程序在顺序一致性模型中执行时,整体是无序的,其执行结果往往无法预知。而且,保证未同步程序在这两个模型中的执行结果一致没什么
意义。
未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有如下几个差异。
1)顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
2)顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
3)JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。
第3个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(Bus Transaction)。总线事务包括读事务(Read Transaction)和写事务(Write Transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写。下面,让我们通过一个示意图来说明总线的工作机制,如图3-14所示。

图3-14 总线的工作机制
由图可知,假设处理器A,B和C同时向总线发起总线事务,这时总线仲裁(Bus Arbitration)会对竞争做出裁决,这里假设总线在仲裁后判定处理器A在竞争中获胜(总线仲裁会确保所有处理器都能公平的访问内存)。此时处理器A继续它的总线事务,而其他两个处理器则要等待处理器A的总线事务完成后才能再次执行内存访问。假设在处理器A执行总线事务期间(不管这个总线事务是读事务还是写事务),处理器D向总线发起了总线事务,此时处理器D的请求会被总线禁止。
总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。
在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销。为了照顾这种处理器,Java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的写操作具有原子性。当JVM在这种处理器上运行时,可能会把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性。
当单个内存操作不具有原子性时,可能会产生意想不到后果。请看示意图,如图3-15所示。

图3-15 总线事务执行的时序图
如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时,处理器B中64位的读操作被分配到单个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半”的无效值。
注意,在JSR -133之前的旧内存模型中,一个64位long/double型变量的读/写操作可以被拆分为两个32位的读/写操作来执行。从JSR -133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

时间: 2024-11-11 01:36:22

《Java并发编程的艺术》一一3.3 顺序一致性的相关文章

《Java 并发编程的艺术》迷你书

本文源自InfoQ发表的<Java 并发编程的艺术>电子书  作者:方腾飞  序言:张龙 免费下载此迷你书 推荐序 欣闻腾飞兄弟的<聊聊并发>系列文章将要集结成InfoQ迷你书进行发布,我感到非常的振奋.这一系列文章从最开始的发布到现在已经经历了两年多的时间,这两年间,Java世界发生了翻天覆地的变化.Java 7已经发布,而且Java 8也将在下个月姗姗来迟.围绕着JVM已经形成了一个庞大且繁荣的生态圈,Groovy.Scala.Clojure.Ceylon等众多JVM语言在蓬勃

《Java并发编程的艺术》-Java并发包中的读写锁及其实现分析

作者:魏鹏  本文是<Java并发编程的艺术>的样章 1. 前言 在Java并发包中常用的锁(如:ReentrantLock),基本上都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞.读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升. 除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式.假设在程序中定义一个共享

《Java并发编程的艺术》源码下载

<Java并发编程的艺术>纸质书购买地址=>天猫(价最低)  当当 京东  互动   亚马逊 <Java并发编程的艺术>电子书购买地址=>亚马逊 请使用JDK1.7及其以上版本编译源码,源码的任何问题可以通过评论告诉我们. 通过附件下载源码:ArtConcurrentBook 从SVN check out 源码:http://code.taobao.org/svn/ifevebook/trunk/ArtConcurrentBook   转载自 并发编程网 - ifeve

《Java并发编程的艺术》一一2.1 volatile的应用

2.1 volatile的应用 在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性".可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值.如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度.本文将深入分析在硬件层面上Intel处理器是如何实现volatile的,

《Java并发编程的艺术》一一3.5 锁的内存语义

3.5 锁的内存语义 众所周知,锁可以让临界区互斥执行.这里将介绍锁的另一个同样重要,但常常被忽视的功能:锁的内存语义.3.5.1 锁的释放-获取建立的happens-before关系 锁是Java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 下面是锁释放-获取的示例代码. class MonitorExample { int a = 0; public synchronized void writer() { // 1 a++; //

《Java并发编程的艺术》一一1.1 上下文切换

1.1 上下文切换 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制.时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms). CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务.但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态.所以任务从保存到再加载的过程就是一次上下文切换. 这就像我们同时

《Java并发编程的艺术》一一2.4 本章小结

2.4 本章小结 本章我们一起研究了volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于本章介绍的volatile和原子操作的实现原理,了解这些原理对我们进行并发编程会更有帮助.

《Java并发编程的艺术》一一2.2 synchronized的实现原理与应用

2.2 synchronized的实现原理与应用 在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁.但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了.本文详细介绍Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程.先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁.具体表现为以下3种形式.对于普通同步方法,

《Java并发编程的艺术》一一3.1 Java内存模型的基础

3.1 Java内存模型的基础 3.1.1 并发编程模型的两个关键问题在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递.在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信.在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信.同步是指程序中用于控制不同线程间操作发生相对

《Java并发编程的艺术》一一1.3 资源限制的挑战

1.3 资源限制的挑战 (1)什么是资源限制资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源.例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制.硬件资源限制有带宽的上传/下载速度.硬盘读写速度和CPU的处理速度.软件资源限制有数据库的连接数和socket连接数等.(2)资源限制引发的问题在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变