Java内存模型的顺序一致性问题

简介: Java内存模型的顺序一致性问题


一、数据竞争与顺序一致性的保证


当程序未正确同步时,就可能会存在数据竞争。


Java 内存模型规范对数据竞争的定义如下:


在一个线程中写一个变量

在另一个线程读同一个变量

而且写和读没有通过同步来排序。

当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(上篇的示例正是如此);


如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序,程序的执行将具有顺序一致性。


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


如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)–即程序的执行结果与该程序在顺序一致性内存模型中的执行结 果相同;


下面你将会看到,这对于程序员来说是一个极强的保证。这里的同步是指广义上的同步,包括对常用同步原语(synchronized,volatile 和 final) 的正确使用。


二、 顺序一致性内存模型


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


顺序一致性内存模型有两大特性:


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


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


image.png


在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读 /写操作;


从上面的示意图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读 /写操作串行化(即在顺序一致性模型中,所有操作之间具有全序关系);


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


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


假设这两个线程使用监视器锁来正确同步:A 线程的三个操作执行后释放监视器 锁,随后 B 线程获取同一个监视器锁。


那么程序在顺序一致性模型中的执行效果将 如下图所示:


image.png


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


image.png


未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序;


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


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


image.png


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


下面我们对前面的示例程序 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 规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同;


下面是该程序在两个内存模型中的执行时序对比图:


image.png


在顺序一致性模型中,所有操作完全按程序的顺序串行执行;


在 JMM 中,临界 区内的代码可以重排序(但 JMM 不允许临界区内的代码“逸出”到临界区之外, 那样会破坏监视器的语义)。JMM 会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图(具体细节后文会说明);


虽然线程 A 在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程 B 根本无法“观察”到线程 A 在临界区内的重排序。 这种重排序既提高了执行效率,又没有改变程序的执行结果;


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


四、未同步程序的执行特性


对于未同步或未正确同步的多线程程序,JMM 只提供最小安全性:


线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false), JMM 保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来;


image.png


为了实现最小安全性,JVM 在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM 内部会同步这两个操作);


因此,在已清零的内存空间(prezeroed memory)分配对象时,域的默认初始化已经完成了;


JMM 不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一 致。因为如果想要保证执行结果一致,JMM 需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响。而且未同步程序在顺序一致性模型中执行时,整体是无序的,其执行结果往往无法预知。保证未同步程序在这两个模型中的执行结果一致没什么意义;


未同步程序在 JMM 中的执行时,整体上是无序的,其执行结果无法预知;


未同步程序在两个模型中的执行特性有下面几个差异:


顺序一致性模型保证单线程内的操作会按程序的顺序执行,而 JMM 不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。


顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。


JMM 不保证对 64 位的 long 型和 double 型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。


第 3 个差异与处理器总线的工作机制密切相关;


在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(bus transaction);


总线事务包括:


  • 读事务 (read transaction)
  • 写事务(write transaction)


读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物 理上连续的字;


这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和 I/O 设备执行内存的读/ 写;


下面让我们通过一个示意图来说明总线的工作机制:


image.png


如上图所示,假设处理器 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 位变量的写将不具有原子性。 当单个内存操作不具有原子性,将可能会产生意想不到后果;


请看下面示意图:


image.png


如上图所示,假设处理器 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 中都必须具有原子性(即任意读操作必须要在单个读事务中执行);


结束语


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。


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

热门文章

最新文章