我们一起学并发编程:Java内存模型(三)顺序一致性
简介:
顺序一致性内存模型是一个理论参考模型,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。
1、数据竞争和顺序一致性当程序未正确同步时,就可能存在数据竞争。
1.1Java内存模型规范对数据竞争的定义如下在一个线程中写一个变量
在另一个线程中读同一个变量
写和读没有通过同步来排序
如果一个多线程程序能够正确同步,这个程序将是一个没有数据竞争的程序,往往存在数据竞争的程序,运行结果与我们的预期结果都会存在偏差。
1.2JMM对多线程程序的内存一致性做的保证如果程序正确同步(正确使用synchronized、volatile和final),程序的执行将具有顺序一致性(SequentiallyConsistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。
2、顺序一致性内存模型2.1特性一个线程中的所有操作必须按照程序的执行顺序来执行
(不管是否正确同步)所有的线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行且立刻对所有线程可见。
图示:
顺序一致性内存模型视图
在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存的读/写操作。上图中可以看出,在任意时刻最多只有一个线程可以连接到内存。因此,在多线程并发执行时,图中的开关装置能把所有的内存读/写操作串行化(即在顺序一致性模型中所有操作之间具有全序关系)。
2.2举例说明顺序一致性模型假设两个线程A和B并发执行。其中
A线程的操作在程序中的顺序为:A1-A2-A3
B线程的操作在程序中的顺序为:B1-B2-B3。
假设线程A和线程B使用监视器锁来正确同步,A线程的3个操作执行后释放监视器锁,随后B线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果如下所示:
顺序一致性模型的一种执行效果
假设线程A和线程B没有做同步,那么这个未同步的程序在顺序一致性模型中的另一种可能的效果如下所示:
顺序一致性模型的另一种执行效果
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但是所有线程都只能看到一个一直的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:A1-B1-A2-B2-A3-B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。这种情况就会出现多种运行结果。
2.3同步程序的顺序一致性效果对上一章的ReorderExample程序用锁来同步
packagecom.lizba.p1;/***<p>*同步示例*</p>**@Author:Liziba*@Date:2021/6/821:44*/publicclassSynReorderExample{//定义变量ainta=0;//flag变量是个标记,用来标志变量a是否被写入booleanflag=false;publicsynchronizedvoidwriter(){//获取锁a=1;flag=true;}//释放锁publicsynchronizedvoidreader(){//获取锁if(flag){inti=a*a;System.out.println("i:"+i);}}//释放锁}测试代码
/***测试**@paramargs*/publicstaticvoidmain(String[]args){finalSynReorderExamplere=newSynReorderExample();newThread(){publicvoidrun(){re.writer();}}.start();newThread(){publicvoidrun(){re.reader();}}.start();}执行多次结果结果都为1
总结
在上面的示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性内存模型中的执行结果相同。
顺序一致性模型中和JMM内存模型中的执行时序图
总结
在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区的代码“逸出”到临界区之外,那样会破坏监视器锁的语义)。JMM会在进入临界区和退出临界区的关键时间点做一些特殊处理,使得线程在这两个时间点具有顺序一致性模型中相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视锁互斥执行的特性,这里线程B无法“观察”到线程A在临界区内的重排序。JMM在具体实现上的基本方针为:在不改变(正确同步)程序执行结果的前提下,尽可能为编译器和处理器的优化打开方便大门。
2.4未同步程序的执行特性对于未同步或者未正确同步(代码写错了的兄弟们),JMM只提供最小的安全性:
线程执行时读取到的值不会无中生有(OutOfThinAir)
之前某个线程写入的值
默认值(0、Null、False)--JVM会在已经清零了内存空间(Pre-zeroedMemory)分配对象。
未同步程序在两个模型中的执行特性对比
比较内容\模型名称顺序一致性模型JMM模型单线程内顺序执行√×一致的操作执行顺序√×64位long型和double型变量写原子性√×第三个差异和总线的机制有关。在一些32位处理器上,处理64位的数据写操作,需要将一个写操作拆分为两个32位的写操作。
3、64位long型和double型变量写原子性3.1CPU、内存和总线简述在计算机中,数据通过总线在处理器和内存之间传递,每次处理器和内存之间的数据传递都是通过一系列的步骤来完成的,这一系列的步骤称之为总线事务(BusTransaction)。总线事务包括读事务(ReadTransaction)和写事务(WriteTransaction),事务会读\写内存中一个或多个物理上连续的字。
读事务→内存到处理器
写事务→处理器到内存
重点是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他处理器和I\O设备执行内存的读\写。
图示:
总线工作机制
由上图所示,假设处理器A、B、C、D同时向总线发起总线事务,这时总线总裁(BusArbitration)会对竞争作出裁决,这里假设处理器A在竞争中获胜(总线仲裁会确保所有处理器能公平访问内存)。此时处理器A继续它的总线事务,而其他所有的总线事务必须要等待A的事务完成才能再次执行内存的读\写操作。总线事务工作机制确保处理器对内存的访问以串行的方式执行。在任意时间点都只有一个处理器可以访问内存,这个特性能确保总线事务之间的内存读\写操作具有原子性。
3.2long和double类型的操作在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,那么会有非常大的同步开销。Java语言规范中鼓励但不强求JVM对64位long型和double类型的变量写操作具有原子性。当JVM在这种处理器上运行时,会把一个64位的变量写操作拆成两个32位写操作来执行,此时写不具备原子性。
图示:
总线事务执行的时序图
存在问题:
假设处理器A写一个long类型的变量,同时处理器B要读这个long类型的变量。处理器A中64位的写操作被拆分成两个32位的写操作,且这两个32位的写操作被分配到不同的事务中执行。此时,处理器B中64位的读操作被分配到单个读事务中执行。如果按照上面的执行顺序,那么处理器B读取的将会是一个不完整的无效值。
处理方式:
JSR-133内存模型开始(JDK1.5),写操作能拆分成两个32位写事务执行,读操作必须在单个事务中执行。
文章总结至《Java并发编程艺术》,下篇总结“volatile的内存语义”,敬请关注。
我们一起学并发编程:Java内存模型(一)基础
Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(LocalMemory),本地内存中存储了该线程以读\/写共享变量的副本。本地内存时JMM的一个抽...
我们一起学并发编程:Java内存模型(三)顺序一致性
在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存的读\/写操作。上图中可以看出,在任意时刻最多只有一个线程可以连接到内存。因此,在多线程并发执行时,图中的开关装置能把所有的内存读\/写操作串行化(即在顺序一致性...
我们一起学并发编程:Java内存模型(八)双重检查锁定与延迟初始化_百度知 ...
在Java语言规范中,首次发生如下情况中的任意一种,一个类或者一个接口类型T将会被立即初始化: T是一个类,而且一个T类型的实例被创建 T是一个类,且T中声明的一个静态方法被调用 T中声明的一个静态字段被赋值 T中声明的一个静态字段被使用,而且这个字段不是一个常量字段 T是一个顶级类(TopLevelClass),而且一...
Java内存模型(JMM)详解
在Java内存模型中,所有变量都被存储在主内存(Main Memory)中,与硬件结构中的主内存不同,它是虚拟机内存的一部分。每个线程拥有自己的工作内存,其中包含了主内存中的变量副本。线程的所有操作都在工作内存中进行,不能直接访问主内存中的数据。不同线程间变量的传递需通过主内存完成,形成线程、主内...
Java内存模型(JMM)详解
Java内存模型(JMM)是Java语言为解决跨平台并发问题而制定的一套规范,它定义了线程与主内存之间的交互规则,确保了多线程环境下的内存一致性。Java内存模型并非硬件内存的一部分,而是抽象的编程概念,它将内存划分为逻辑上的主内存和工作内存,其中主内存是所有线程共享的,而工作内存则是每个线程自己的...
深入理解Java内存模型JMM
了解了Java内存模型之后,你将能够更好地理解并发编程中的原子性、有序性和可见性问题,并在实际编程中正确使用相关关键字,如volatile、synchronized和final。通过深入学习《深入理解Java虚拟机》和《Java并发编程的艺术》等书籍,你将能够更全面地掌握Java并发编程的知识体系,构建自己的知识框架。如果你在...
Java多线程内存读写 —— 内存屏障的理解
读屏障(如lfence)确保前一个读操作读取的数据在后续读操作前完成;写屏障(如sfence)确保前一个写操作刷入的数据在后续读操作前可见。通过这些指令,程序能在多核环境下实现更一致的内存访问。Java内存模型中的四种读写屏障对应了读读(LoadLoad)、读写(LoadStore)、写写(StoreStore)、写读(...
架构师需要掌握哪些知识
架构师需要掌握的知识如下:1、并发编程:JAVA内存模型(JMM)、java当中的线程通讯和消息传递、Synchronized的概念和分析、Volatile和DCL的知识、并发基础之AQS的深度分析、原子操作常用知识讲解。2、框架和源码应用:mybatis应用和源码解析、tomcat源码解析、spring源码分析。3、Spring微服务:Spring Cloud 、...
05.深入理解JMM和Happens-Before
深入理解JMM和Happens-Before 在编程的世界中,Java内存模型(JMM)与Happens-Before规则是理解并发编程的关键。本文将深入探讨这两个概念,揭示它们在多线程编程中的作用以及它们如何协同工作以确保程序的正确执行。首先,JMM(Java Memory Model)定义了Java程序中内存操作的可见性规则。它描述了多线程环境下...
java编程的actor模式如何实现?
Actor类是抽象类,处理消息的handleMessage方法为抽象方法,需要每个具体类来重载实现。Node代表节点,与Skynet中节点意义相同,它是一个独立的Java进程,有自己的IP和端口,Node之间通过异步的网络通信发送和接收消息。一个Node中可以运行多个Actor,一个Actor仅可与一个Node绑定。ActorSystem是Actor的管理系统...