`

【转】深入理解Java内存模型(三)——顺序一致性

阅读更多

当程序未正确同步时,就会存在数据竞争。java内存模型规范对数据竞争的定义如下:

  • 在一个线程中写一个变量,
  • 在另一个线程读同一个变量,
  • 而且写和读没有通过同步来排序。

当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(前一章的示例正是如此)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。

JMM对正确同步的多线程程序的内存一致性做了如下保证:

  • 如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)--即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同(马上我们将会看到,这对于程序员来说是一个极强的保证)。这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile和final)的正确使用。

顺序一致性内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

顺序一致性内存模型为程序员提供的视图如下:

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

为了更好的理解,下面我们通过两个示意图来对顺序一致性模型的特性做进一步的说明。

假设有两个线程A和B并发执行。其中A线程有三个操作,它们在程序中的顺序是:A1->A2->A3。B线程也有三个操作,它们在程序中的顺序是:B1->B2->B3。

假设这两个线程使用监视器来正确同步:A线程的三个操作执行后释放监视器,随后B线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示:

现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图:

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

同步程序的顺序一致性效果

下面我们对前面的示例程序ReorderExample用监视器来同步,看看正确同步的程序如何具有顺序一致性。

请看下面的示例代码:

class SynchronizedExample {
int a = 0;
boolean flag = false;

public synchronized void writer() {
    a = 1;
    flag = true;
}

public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
}
}

上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

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

从这里我们可以看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。

未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在以清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成了。

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。保证未同步程序在两个模型中的执行结果一致毫无意义。

和顺序一致性模型一样,未同步程序在JMM中的执行时,整体上也是无序的,其执行结果也无法预知。同时,未同步程序在这两个模型中的执行特性有下面几个差异:

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
  3. JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

第3个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(bus transaction)。总线事务包括读事务(read transaction)和写事务(write transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和I/O设备执行内存的读/写。下面让我们通过一个示意图来说明总线的工作机制:

如上图所示,假设处理器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位变量的读/写将不具有原子性。

当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图:

如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时处理器B中64位的读操作被拆分为两个32位的读操作,且这两个32位的读操作被分配到同一个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。

参考文献

  1. JSR-133: Java Memory Model and Thread Specification
  2. Shared memory consistency models: A tutorial
  3. The JSR-133 Cookbook for Compiler Writers
  4. 深入理解计算机系统(原书第2版)
  5. UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers
  6. The Java Language Specification, Third Edition

作者简介

程晓明,Java软件工程师,国家认证的系统分析师、信息项目管理师。专注于并发编程,就职于富士通南大。个人邮箱:asst2003@163.com

 

转自:http://www.infoq.com/cn/articles/java-memory-model-3?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_articles_clk

分享到:
评论

相关推荐

    Java内存模型知识汇总

    本文将对Java内存模型进行知识汇总,帮助读者更好地理解和掌握相关知识点。 首先,内存模型的概念是与计算机硬件密切相关的。计算机执行程序时,CPU执行指令并需要频繁地与数据进行交互,而这些数据存储在主存(即...

    java内存模型(有助理解多线程)

    JSR133是Java内存模型的重要更新之一,它的目标是解决早期版本Java内存模型中的不一致性问题。具体而言,JSR133旨在: - **保留类型安全**:确保Java程序的类型安全性,即使是在进行了各种优化的情况下。 - **...

    JAVA内存模型——同步操作规则1

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中用于定义程序中各个变量(包括实例字段、静态字段和局部变量)的可见性和有序性的一种抽象概念。它为多线程环境下如何保证数据一致性提供了理论基础。下面将...

    java网络程序——QQ聊天程序

    数据库设计需要考虑性能、扩展性和一致性,可能采用关系型数据库(如MySQL)或NoSQL数据库(如MongoDB)。 9. **RESTful API** - 为了实现客户端和服务器之间的松耦合,QQ聊天程序可能会采用RESTful API设计,使得...

    10 有福同享,有难同当—原子性.pdf

    Java并发编程是软件开发中的重要领域,特别是在多核处理器和...同时,深入理解Java内存模型对于优化并发程序和避免潜在问题至关重要。在后续章节中,我们将更深入地探讨这些问题,以便更好地应对并发编程中的挑战。

    Java并发理论,如何理解线程安全.docx

    线程安全是并发编程的核心概念,确保在多线程环境中代码的正确性和一致性。 **线程安全的理解** 线程安全问题通常发生在多线程环境中,当多个线程同时访问和修改同一份资源时,如果没有适当的同步措施,可能导致...

    【并发编程】深入理解JMM.pdf

    ### 并发编程深入理解——基于JMM的记忆模型解析 #### 一、基本概念与并发编程基础 在深入了解Java内存模型(JMM)之前,我们首先需要掌握一些基本的并发编程概念,这包括进程、线程以及并行与并发的区别。 1. **...

    SSM实战项目——Java高并发秒杀API.zip

    SSM实战项目——Java高并发秒杀API是一个典型的Java Web...以上是SSM实战项目——Java高并发秒杀API中可能涉及的技术点,通过这个项目,开发者可以深入理解Java Web开发,并提升在高并发环境下的系统设计和优化能力。

    Expain of Brian Goetz about java memory model

    ### 关于Java内存模型的理解——Brian Goetz的观点 #### 关于演讲者——Brian Goetz Brian Goetz是一位具有18年专业软件开发经验的资深专家。他不仅是超过60篇关于Java开发文章的作者(可访问他的个人网站...

    java第十章答案JAVA多线程——一篇文章让你彻底征服多线程开发

    - **线程安全**:指在并发环境中,即使线程调度顺序不同,代码执行的结果也是一致的。实现线程安全的方法之一是使用同步机制。 #### 四、线程生命周期 线程在其生命周期中会经历以下几种状态: - **新建** (`NEW`)...

    Java并发编程:volatile关键字解析

    在深入了解`volatile`关键字之前,我们首先需要理解计算机内存模型的一些基本概念。在现代计算机系统中,CPU为了提高执行效率,会将频繁访问的数据从较慢的主存复制到更快的CPU缓存中。这种做法虽然提高了性能,但也...

    聊聊并发(一)深入分析Volatile的实现原理

    首先,我们需要理解Java内存模型(JMM,Java Memory Model),它是Java语言规范定义的一种抽象概念,用于描述所有线程如何共享和访问内存。在JMM中,每个线程都有自己的工作内存,而主内存是所有线程共享的数据存储...

    java 并发编程实践

    本篇文章将深入探讨Java并发编程的相关知识点,主要基于提供的两个文件——"Java并发编程实战(中文版).pdf"和"Java Concurrency in Practice.pdf"。 1. **线程与并发** - **线程基础**:Java中的线程是并发执行...

    Java网络编程学习资料

    在分布式环境中,事务处理是确保数据一致性的重要手段。JTA是Java平台的标准API,允许应用程序在不同的资源管理器(如数据库、消息队列)之间进行分布式事务处理。JTA定义了事务管理器和资源管理器之间的接口,使得...

    Java-In-A-Nutshell-5th-Edition(2)

    这本书涵盖了Java语言的核心概念、语法、工具和最佳实践,旨在帮助读者快速掌握并深入理解Java编程。 1. **Java语言基础** - 类与对象:Java是面向对象的语言,以类为构造单元,通过对象来封装数据和行为。 - ...

    Java——volatile关键字详解

    `volatile`变量的读写操作前后会插入内存屏障,确保在内存操作前后不会发生指令的乱序,这对于保持数据的一致性至关重要。 **使用场景:** 1. **状态标记量**:当一个线程需要根据某个状态决定是否继续执行时,可以...

    文件复制——多线程

    7. **安全性与一致性**:在并发复制过程中,要确保文件的完整性和一致性,避免数据丢失或损坏。可以使用文件的校验和(如MD5或SHA-1)来验证复制后的文件是否与原文档一致。 综上所述,文件复制的多线程实现涉及了...

    秋招面经和总结(csdn)————程序.pdf

    18. **分布式事务**:处理分布式系统中多个数据库或服务间的事务一致性问题,如2PC、TCC、Saga等模式。 19. **锁机制**:包括synchronized、ReentrantLock、读写锁、信号量等,理解和掌握它们在并发场景中的应用。 ...

    tomcat 类加载机制 —— ClassLoader

    这样保证了基础类库的一致性,同时也允许Web应用覆盖这些基础类。 在Tomcat中,我们可以通过配置`catalina.properties`文件和`server.xml`文件来调整类加载策略,例如设置自定义的类加载顺序或启用共享类加载器。 ...

    尚硅谷面试题第二季1

    Java内存模型(JMM)是Java虚拟机为了解决多线程环境下内存一致性问题而提出的一种抽象模型。它定义了线程如何访问共享变量以及它们之间的通信规则。JMM主要有以下规定: - 线程解锁前,必须将共享变量的最新值刷新...

Global site tag (gtag.js) - Google Analytics