【并发编程的艺术】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域的内存语义,来看它们是怎样解决这些问题的,以及各自的适用场景。

相关文章
|
1天前
|
缓存 Java 数据库
Java并发编程学习11-任务执行演示
【5月更文挑战第4天】本篇将结合任务执行和 Executor 框架的基础知识,演示一些不同版本的任务执行Demo,并且每个版本都实现了不同程度的并发性。
20 4
Java并发编程学习11-任务执行演示
|
2天前
|
缓存 Java 数据库
Java并发编程中的锁优化策略
【5月更文挑战第9天】 在高负载的多线程应用中,Java并发编程的高效性至关重要。本文将探讨几种常见的锁优化技术,旨在提高Java应用程序在并发环境下的性能。我们将从基本的synchronized关键字开始,逐步深入到更高效的Lock接口实现,以及Java 6引入的java.util.concurrent包中的高级工具类。文中还会介绍读写锁(ReadWriteLock)的概念和实现原理,并通过对比分析各自的优势和适用场景,为开发者提供实用的锁优化策略。
3 0
|
2天前
|
算法 安全 Java
深入探索Java中的并发编程:CAS机制的原理与应用
总之,CAS机制是一种用于并发编程的原子操作,它通过比较内存中的值和预期值来实现多线程下的数据同步和互斥,从而提供了高效的并发控制。它在Java中被广泛应用于实现线程安全的数据结构和算法。
17 0
|
3天前
|
存储 安全 算法
掌握Java并发编程:Lock、Condition与并发集合
掌握Java并发编程:Lock、Condition与并发集合
11 0
|
3天前
|
存储 算法 Java
了解Java内存管理与垃圾回收机制
了解Java内存管理与垃圾回收机制
6 0
|
3天前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第8天】在Java并发编程中,锁是实现线程同步的关键机制。为了提高程序的性能,我们需要对锁进行优化。本文将介绍Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级和读写锁等方法,以帮助开发者提高多线程应用的性能。
|
4天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第7天】在Java中,多线程编程是提高应用程序性能和响应能力的关键。本文将深入探讨Java并发编程的核心概念,包括线程安全、同步机制以及性能优化策略。我们将通过实例分析,了解如何避免常见的并发问题,如死锁、竞态条件和资源争用,并学习如何使用Java提供的并发工具来构建高效、可靠的多线程应用。
|
2天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
10 1
|
1天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
|
1天前
|
安全 Java
【JAVA进阶篇教学】第六篇:Java线程中状态
【JAVA进阶篇教学】第六篇:Java线程中状态