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

相关文章
|
5天前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
JVM简介—1.Java内存区域
|
3月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
60 0
|
17天前
|
存储 IDE Java
java设置栈内存大小
在Java应用中合理设置栈内存大小是确保程序稳定性和性能的重要措施。通过JVM参数 `-Xss`,可以灵活调整栈内存大小,以适应不同的应用场景。本文介绍了设置栈内存大小的方法、应用场景和注意事项,希望能帮助开发者更好地管理Java应用的内存资源。
29 4
|
22天前
|
Java Shell 数据库
【YashanDB 知识库】kettle 同步大表提示 java 内存溢出
【问题分类】数据导入导出 【关键字】数据同步,kettle,数据迁移,java 内存溢出 【问题描述】kettle 同步大表提示 ERROR:could not create the java virtual machine! 【问题原因分析】java 内存溢出 【解决/规避方法】 ①增加 JVM 的堆内存大小。编辑 Spoon.bat,增加堆大小到 2GB,如: if "%PENTAHO_DI_JAVA_OPTIONS%"=="" set PENTAHO_DI_JAVA_OPTIONS="-Xms512m" "-Xmx512m" "-XX:MaxPermSize=256m" "-
|
3月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
3月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
3月前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
3月前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
70 0
|
17天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
142 60
【Java并发】【线程池】带你从0-1入门线程池
|
6天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
58 23