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

相关文章
|
9月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
1069 3
|
10月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
8月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
243 4
|
8月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
9月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
1009 17
|
10月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
542 2
|
11月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
557 0
|
10月前
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
10月前
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
316 0
|
11月前
|
存储 Java
Java对象的内存布局
在HotSpot虚拟机中,Java对象的内存布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头包含Mark Word、Class对象指针及数组长度;实例数据存储对象的实际字段内容;对齐填充用于确保对象大小为8字节的整数倍。
218 0