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

当程序未正确同步时,就会存在数据竞争。Java内存模型规范对数据竞争的定义如下:

  • 在一个线程中写一个变量,
  • 在另一个线程读同一个变量,
  • 而且写和读没有通过同步来排序。

当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果(前一章的示例正是如此)。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。

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

  • 如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)--即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同(马上我们将会看到,这对于程序员来说是一个极强的保证)。这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile和final)的正确使用。

顺序一致性内存模型

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

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

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

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

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

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

假设这两个线程使用监视器来正确同步:A线程的三个操作执行后释放监视器,随后B线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示:

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

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

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

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

下面我们对前面的示例程序ReorderExample用监视器来同步,看看正确同步的程序如何具有顺序一致性。

请看下面的示例代码:

[java] view plaincopy
  1. class SynchronizedExample {
  2. int a = 0;
  3. boolean flag = false;
  4. public synchronized void writer() {
  5. a = 1;
  6. flag = true;
  7. }
  8. public synchronized void reader() {
  9. if (flag) {
  10. int i = a;
  11. ……
  12. }
  13. }
  14. }

上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

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

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

未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在以清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成了。

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。保证未同步程序在两个模型中的执行结果一致毫无意义。

和顺序一致性模型一样,未同步程序在JMM中的执行时,整体上也是无序的,其执行结果也无法预知。同时,未同步程序在这两个模型中的执行特性有下面几个差异:

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
  3. JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

第3个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(bus transaction)。总线事务包括读事务(read transaction)和写事务(write transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和I/O设备执行内存的读/写。下面让我们通过一个示意图来说明总线的工作机制:

如上图所示,假设处理器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位变量的读/写将不具有原子性。

当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图:

如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时处理器B中64位的读操作被拆分为两个32位的读操作,且这两个32位的读操作被分配到同一个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。

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

参考文献

  1. JSR-133: Java Memory Model and Thread Specification
  2. Shared memory consistency models: A tutorial
  3. The JSR-133 Cookbook for Compiler Writers
  4. 深入理解计算机系统(原书第2版)
  5. UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers
  6. The Java Language Specification, Third Edition

深入理解JMM(Java内存模型) --(三)顺序一致性相关推荐

  1. JMM Java内存模型的概念以及happens-before原则

    详细介绍了JMM Java内存模型的概念.由来,以及happens-before原则的具体规则. Java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的一组规范以及机 ...

  2. 面试题:请介绍⼀下 JMM(Java 内存模型)

    面试题:请介绍⼀下 JMM(Java 内存模型) 关键词 CPU缓存一致性协议(例如MESI),多个CPU核心之间缓存不会出现不同步的问题 Store Buffer.Load Buffer和L1之间却 ...

  3. 重点知识学习(8.2)--[JMM(Java内存模型),并发编程的可见性\原子性\有序性,volatile 关键字,保持原子性,CAS思想]

    文章目录 1.JMM(Java Memory Model) 2.并发编程的可见性 3.并发编程的有序性 4.并发编程的原子性 5.volatile 关键字 6.保持原子性: 加锁,JUC原子类 加锁 ...

  4. 深入理解 Java 内存模型 JMM

    前提 <深入理解 Java 内存模型>程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结.全书页数虽不多,内容讲得挺深的.细看的 ...

  5. 聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型

    可以说并发系统要解决的最核心问题之一就是一致性的问题,关于一致性的研究已经有几十年了,有大量的理论,算法支持.这篇说说一致性这个主题一些经常提到的概念,理清Java内存模型在其中的位置. 一致性问题更 ...

  6. 从底层吃透java内存模型(JMM)、volatile、CAS

    前言 随着计算机的飞速发展,cpu从单核到四核,八核.在2020年中国网民数预计将达到11亿人.这些数据都意味着,作为一名java程序员,必须要掌握多线程开发,谈及多线程,绕不开的是对JMM(Java ...

  7. 全面理解java内存模型_深入理解Java内存模型(八)——总结

    处理器内存模型 顺序一致性内存模型是一个理论参考模型,JVM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照.JVM和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序 ...

  8. 深入理解 Java 内存模型(转载)

    摘要: 原创出处 http://www.54tianzhisheng.cn/2018/02/28/Java-Memory-Model/ 「zhisheng」欢迎转载,保留摘要,谢谢! 0. 前提 &l ...

  9. Java高并发编程(三):Java内存模型

    1 Java内存模型的基础 在并发编程里,需要处理两个问题: 线程之间如何通信 线程之间如何同步. 通信指的是线程之间以何种机制来交换信息.在命令式编程里中,线程之间的通信机制有两种:共享内存和消息传 ...

  10. 深入理解 Java内存模型

    深入理解 Java内存模型 原文地址:http://www.54tianzhisheng.cn/2018/02/28/Java-Memory-Model/ 本文主要内容有 Java 内存模型的基础.重 ...

最新文章

  1. RocketMQ源码 — 十一、 RocketMQ事务消息
  2. 简单的链式语法及实际运用(链式编程的基本原理)
  3. 齐家网困境:进退两难,合作方和消费者两头都不讨好?
  4. Spyder打开报错解决办法
  5. WPF使用Webbrowser操作网页的主要代码
  6. (2环境架设)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练
  7. kali怎么开启php服务器,kali开启端口、关闭防火墙方法
  8. JavaScript 函数replace揭秘
  9. Xamarin开发Android时Visual Studio 2012没有智能提示解决办法
  10. 漂亮女生应聘华为的真实过程
  11. 人工智能视频教程下载
  12. 织梦DEDE采集文章标题过短,自带采集标题限制,解决文章标题字数长度方法
  13. PS计算机字体Q,PS q萌字体教程
  14. 实战演习(四)——网络流量系统分析简介
  15. samsung 手机webview 加载https 页面不显示问题
  16. c++switch语言,C++ switch语句
  17. 是时候使用Markdown写作了
  18. 微信卡券怎么制作以及卡券封号推送消息技术分享
  19. windows重装系统
  20. c#考试,答题系统制作(窗体)

热门文章

  1. 有序充电matlab仿真,电动汽车有序充电策略研究
  2. 忘了微信密码怎么办_微信支付密码怎么改?微信支付密码忘了怎么办?详细教程来了!...
  3. OpenCV2:Mat介绍及常见用法
  4. java打卡-day3 变量和数据类型
  5. 从0到1开发自动化测试框架(硬货太多,建议阅读)
  6. python调用函数获取最开始的异常_Python使用sys.exc_info()方法获取异常信息
  7. javaweb简单的登录增删改查系统_国产化之路统信UOS /Nginx /Asp.Net Core+ EF Core 3.1/达梦DM8实现简单增删改查操作...
  8. 怎样实现banner自动播放html,纯css实现轮播图banner自动轮换效果
  9. linux启动中继服务器失败,Tor中继服务器在Linux里安装配置的流程
  10. mysql经典sql语句大全_经典SQL语句大全