【并发编程的艺术】Java内存模型的顺序一致性

简介: 首先明确一点,顺序一致性内存模型是一个被理想化了的理论参考模型,提供了很强的内存可见性保证。其两大特性如下:1)一个线程中的所有操作,必须按照程序的顺序来执行(代码编写顺序)2)无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序。每个操作都必须原子执行且立即对所有线程可见。

系列文章:

【并发编程的艺术】JVM 体系与内存模型

【并发编程的艺术】JAVA 并发机制的底层原理

【并发编程的艺术】JAVA 原子操作实现原理

【并发编程的艺术】JVM 内存模型

【并发编程的艺术】详解指令重排序与数据依赖

一 概念

   首先明确一点,顺序一致性内存模型是一个被理想化了的理论参考模型,提供了很强的内存可见性保证。其两大特性如下:

1)一个线程中的所有操作,必须按照程序的顺序来执行(代码编写顺序)

2)无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序。每个操作都必须原子执行且立即对所有线程可见。

对开发者来说,视图如下:

   从图中可以看出,该模型有一个单一的全局内存,这个内存通过一个开关连接到任意一个线程,同时每个线程都必须按照程序的顺序执行内存的读/写操作。任意时刻,最多只能有一个线程可以连接到内存。当多线程并发时,这个开关会把所有线程的所有内存读/写操作串行化执行。

二 案例示意

   有A、B两个线程并发执行,且各自都有3个操作。A: A1->A2->A3;B:B1->B2->B3。当这两个线程使用监视器锁来保证同步执行:A线程先获取监视器锁;A的3个步骤执行完成后释放监视器锁;B获取同一个监视器锁,B按顺序执行3个操作完成。那么整个程序在顺序一致性模型中的执行顺序应该如下图所示:

但如果没有做同步,那么执行流程可能如下图所示:

在这种情况下,看起来是乱序的,虽然只看A线程或只看B线程依然保持顺序不变。且所有线程都只能看到一个一致的整体执行顺序,即:B1->A1->A2->B2->A3->B3。这点的保证,是因为顺序一致性模型中的每个操作必须立即对任意线程可见

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

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

在回顾一下前面章节中提到过的示例代码,这里会加上同步控制:

public 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;
            //other action ...
        }
    }
}

writer() 和 reader()两个方法是同步方法(通过synchronized关键字标记),两个线程A、B,A执行writer()方法后,B线程执行reader(),这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

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

在顺序一致性模型,和JMM的执行效果如下图所示:

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

四 未同步程序的执行特性

   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)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步视图并并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写。

   当有多个处理器A, B, C同时向总线发起总线事务时,总线仲裁(Bus Arbitration)会对竞争做出裁决。这里假设总线在仲裁后判定A竞争获胜,此时A继续它的总线事务,而其他两个处理器需要等待处理器A的总线事务完成后才能再次执行内存访问。在A执行总线事务期间(无论是读事务还是写事务),其他处理器发起总线事务的请求总会被禁止。

   总线的工作机制把所有处理器对内存的访问串行化执行,在任意时间点,最多只能有一个处理器可以访问内存。这样确保了单个总线事务之中内存的读/写操作具有原子性。

当单个内存操作不具有原子性时,可能会产生意想不到的后果。例如:

   前面提到过,long 和 double是64位,处理器A对long变量的操作会拆成高32位和低32位的两个写操作,且这两个32位的写操作可能被分配到不同的写事务中执行。同时,B中的64位读操作被分配到单个的都市无中执行,当两个处理器中的操作按照上图的时序执行时,处理器B会看到被A”写了一半“的无效值。

注:

   JSR-133之前的旧内存模型中,一个64位的long/double类型变量的读/写操作可以被拆分为两个32位读/写操作来执行。

   JSR-133内存模型开始(JDK5),只允许报一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR-133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

五 总结

   通过本章内容,我们终于深入到了多线程场景,并发执行问题的根源。总线的工作机制,顺序一致性模型的理想情况,以及JMM在性能与一致性上的折衷。通过这些,我们了解到了问题产生的原因。在下一篇文章中,我们将介绍volatile、synchronized、final域的内存语义,来看它们是怎样解决这些问题的,以及各自的适用场景。

相关文章
|
3天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】本文将深入探讨Java并发编程中的一个重要主题——线程池。我们将从线程池的基本概念入手,了解其工作原理和优势,然后详细介绍如何使用Java的Executor框架创建和管理线程池。最后,我们将讨论一些高级主题,如自定义线程工厂和拒绝策略。通过本文的学习,你将能够更好地理解和使用Java的线程池,提高你的并发编程能力。
|
1天前
|
并行计算 安全 Java
Java 并发编程的探索之旅
【5月更文挑战第2天】 在多线程的世界里,程序的行为变得错综复杂。本文将带您走进 Java 并发编程的核心概念,通过深入分析并发工具的使用和原理,帮助您构建高效、安全且响应迅速的应用程序。我们将探讨线程的基本知识,同步机制,以及高级并发工具如 Executors、Futures 和 Streams。通过理论与实践相结合的方式,为开发者提供一份清晰、实用的并发编程指南。
6 2
|
2天前
|
存储 安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第1天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细分析线程安全问题的根源,以及如何通过合理的设计和编码实践来避免常见的并发问题。同时,我们还将探讨如何在保证线程安全的前提下,提高程序的并发性能,包括使用高效的同步机制、减少锁的竞争以及利用现代硬件的并行能力等技术手段。
|
2天前
|
安全 Java 程序员
Java并发编程:理解并应用ReentrantLock
【4月更文挑战第30天】 在多线程的世界中,高效且安全地管理共享资源是至关重要的。本文深入探讨了Java中的一种强大同步工具——ReentrantLock。我们将从其设计原理出发,通过实例演示其在解决并发问题中的实际应用,以及如何比传统的synchronized关键字提供更灵活的锁定机制。文章还将讨论在使用ReentrantLock时可能遇到的一些挑战和最佳实践,帮助开发者避免常见陷阱,提高程序性能和稳定性。
|
2天前
|
缓存 Java 调度
Java并发编程:深入理解线程池
【4月更文挑战第30天】 在Java并发编程中,线程池是一种重要的工具,它可以帮助我们有效地管理线程,提高系统性能。本文将深入探讨Java线程池的工作原理,如何使用它,以及如何根据实际需求选择合适的线程池策略。
|
2天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】 本文将深入探讨Java中的线程池,解析其原理、使用场景以及如何合理地利用线程池提高程序性能。我们将从线程池的基本概念出发,介绍其内部工作机制,然后通过实例演示如何创建和使用线程池。最后,我们将讨论线程池的优缺点以及在实际应用中需要注意的问题。
|
3天前
|
存储 安全 Java
【亮剑】Java并发编程中的四个关键字:ThreadLocal、Volatile、Synchronized和Atomic
【4月更文挑战第30天】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制。`ThreadLocal`为每个线程提供独立变量副本;`Volatile`确保变量可见性,但不保证原子性;`Synchronized`实现同步锁,保证单线程执行;`Atomic`类利用CAS实现无锁并发控制。理解其原理有助于编写高效线程安全代码。根据业务场景选择合适机制至关重要。
|
缓存 算法 Java
全网最硬核 Java 新内存模型解析与实验 - 5. JVM 底层内存屏障源码分析
全网最硬核 Java 新内存模型解析与实验 - 5. JVM 底层内存屏障源码分析
全网最硬核 Java 新内存模型解析与实验 - 5. JVM 底层内存屏障源码分析
全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验(下)
全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验(下)
全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验(下)
|
缓存 Java C++
全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验(中)
全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验(中)
全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验(中)