Java内存模型-JMM简介

Java内存模型-JMM简介

BangQ IT哈哈
本来准备把内存模型单独放到某一篇文章的某个章节里面讲解,后来查阅了国外不少文档才发现其实JVM内存模型的内容还蛮多的,因此直接做为一个章节的基础知识来说解,可能该章节概念的东西比较多。一个开发Java的开发者,一旦了解了JVM内存模型就可以更加深刻地了解该语言的语言特性,可能这个章节更多的是概念,没有太多代码实例,因此但愿读者谅解,本文尽可能涵盖全部Java语言能够碰到的和内存相关的内容,一样也会提到一些和内存相关的计算机语言的一些知识,为草案。由于平时开发的时候没有特殊状况不会进行内存管理,因此有可能有笔误的地方比较多,我用的是Windows平台,因此本文涉及到的与操做系统相关的只是仅仅局限于Windows平台。不只仅如此,这一个章节牵涉到的多线程和另一些内容并无讲到,这里主要是结合JVM内部特性把本章节做为核心的概念性章节来说解,这样方便初学者深刻以及完全理解Java语言)java

1.JMM简介

  i.内存模型概述

  Java平台自动集成了线程以及多处理器技术,这种集成程度比Java之前诞生的计算机语言要厉害不少,该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具备开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,每每容易混淆的一个概念就是内存模型。究竟什么是内存模型?内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,可是编译器、运行库、处理器或者系统缓存能够有特权在变量指定内存位置存储或者取出变量的值。【JMM】(Java Memory Model的缩写)容许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了final或synchronized明确请求了某些可见性的保证。程序员

  1)JSR133:

  在Java语言规范里面指出了JMM是一个比较开拓性的尝试,这种尝试视图定义一个一致的、跨平台的内存模型,可是它有一些比较细微并且很重要的缺点。其实Java语言里面比较容易混淆的关键字主要是synchronized和volatile,也由于这样在开发过程当中每每开发者会忽略掉这些规则,这也使得编写同步代码比较困难。
  JSR133自己的目的是为了修复本来JMM的一些缺陷而提出的,其自己的制定目标有如下几个:编程

  • 保留目前JVM的安全保证,以进行类型的安全检查:
  • 提供(out-of-thin-air safety)无中生有安全性,这样“正确同步的”应该被正式并且直观地定义
  • 程序员要有信心开发多线程程序,固然没有其余办法使得并发程序变得很容易开发,可是该规范的发布主要目标是为了减轻程序员理解内存模型中的一些细节负担
  • 提供大范围的流行硬件体系结构上的高性能JVM实现,如今的处理器在它们的内存模型上有着很大的不一样,JMM应该可以适合于实际的尽量多的体系结构而不以性能为代价,这也是Java跨平台型设计的基础
  • 提供一个同步的习惯用法,以容许发布一个对象使他不用同步就可见,这种状况又称为初始化安全(initialization safety)的新的安全保证
  • 对现有代码应该只有最小限度的影响
      2)同步、异步【这里仅仅指概念上的理解,不牵涉到计算机底层基础的一些操做】:
      在系统开发过程,常常会遇到这几个基本概念,不管是网络通信、对象之间的消息通信仍是Web开发人员经常使用的Http请求都会遇到这样几个概念,常常有人提到Ajax是异步通信方式,那么究竟怎样的方式是这样的概念描述呢?
      同步:同步就是在发出一个功能调用的时候,在没有获得响应以前,该调用就不返回,按照这样的定义,其实大部分程序的执行都是同步调用的,通常状况下,在描述同步和异步操做的时候,主要是指代须要其余部件协做处理或者须要协做响应的一些任务处理。好比有一个线程A,在A执行的过程当中,可能须要B提供一些相关的执行数据,固然触发B响应的就是A向B发送一个请求或者说对B进行一个调用操做,若是A在执行该操做的时候是同步的方式,那么A就会停留在这个位置等待B给一个响应消息,在B没有任何响应消息回来的时候,A不能作其余事情,只能等待,那么这样的状况,A的操做就是一个同步的简单说明。
      异步:异步就是在发出一个功能调用的时候,不须要等待响应,继续进行它该作的事情,一旦获得响应了事后给予必定的处理,可是不影响正常的处理过程的一种方式。好比有一个线程A,在A执行的过程当中,一样须要B提供一些相关数据或者操做,当A向B发送一个请求或者对B进行调用操做事后,A不须要继续等待,而是执行A本身应该作的事情,一旦B有了响应事后会通知A,A接受到该异步请求的响应的时候会进行相关的处理,这种状况下A的操做就是一个简单的异步操做。

      3)可见性、可排序性

      Java内存模型的两个关键概念:可见性(Visibility)和可排序性(Ordering)
      开发过多线程程序的程序员都明白,synchronized关键字强制实施一个线程之间的互斥锁(相互排斥),该互斥锁防止每次有多个线程进入一个给定监控器所保护的同步语句块,也就是说在该状况下,执行程序代码所独有的某些内存是独占模式,其余的线程是不能针对它执行过程所独占的内存进行访问的,这种状况称为该内存不可见。可是在该模型的同步模式中,还有另一个方面:JMM中指出了,JVM在处理该强制实施的时候能够提供一些内存的可见规则,在该规则里面,它确保当存在一个同步块时,缓存被更新,当输入一个同步块时,缓存失效。所以在JVM内部提供给定监控器保护的同步块之中,一个线程所写入的值对于其他全部的执行由同一个监控器保护的同步块线程来讲是可见的,这就是一个简单的可见性的描述。这种机器保证编译器不会把指令从一个同步块的内部移到外部,虽然有时候它会把指令由外部移动到内部。JMM在缺省状况下不作这样的保证——只要有多个线程访问相同变量时必须使用同步。简单总结:
      可见性就是在多核或者多线程运行过程当中内存的一种共享模式,在JMM模型里面,经过并发线程修改变量值的时候,必须将线程变量同步回主存事后,其余线程才可能访问到。
      【*:简单讲,内存的可见性使内存资源能够共享,当一个线程执行的时候它所占有的内存,若是它占有的内存资源是可见的,那么这时候其余线程在必定规则内是能够访问该内存资源的,这种规则是由JMM内部定义的,这种状况下内存的该特性称为其可见性。】
      可排序性提供了内存内部的访问顺序,在不一样的程序针对不一样的内存块进行访问的时候,其访问不是无序的,好比有一个内存块,A和B须要访问的时候,JMM会提供必定的内存分配策略有序地分配它们使用的内存,而在内存的调用过程也会变得有序地进行,内存的折中性质能够简单理解为有序性。而在Java多线程程序里面,JMM经过Java关键字volatile来保证内存的有序访问。数组

      ii.JMM结构:

      1)简单分析:

      Java语言规范中提到过,JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中全部变量都是存在主存中的,对于全部线程进行共享,而每一个线程又存在本身的工做内存(Working Memory),工做内存中保存的是主存中某些变量的拷贝,线程对全部变量的操做并不是发生在主存区,而是发生在工做内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,若是高速缓存不通过内存的时候,也是不可见的一种表现。在Java程序中,内存自己是比较昂贵的资源,其实不只仅针对Java应用程序,对操做系统自己而言内存也属于昂贵资源,Java程序在性能开销过程当中有几个比较典型的可控制的来源。synchronized和volatile关键字提供的内存中模型的可见性保证程序使用一个特殊的、存储关卡(memory barrier)的指令,来刷新缓存,使缓存无效,刷新硬件的写缓存而且延迟执行的传递过程,无疑该机制会对Java程序的性能产生必定的影响。
    Java内存模型-JMM简介
      JMM的最初目的,就是为了可以支持多线程程序设计的,每一个线程能够认为是和其余线程不一样的CPU上运行,或者对于多处理器的机器而言,该模型须要实现的就是使得每个线程就像运行在不一样的机器、不一样的CPU或者自己就不一样的线程上同样,这种状况实际上在项目开发中是常见的。对于CPU自己而言,不能直接访问其余CPU的寄存器,模型必须经过某种定义规则来使得线程和线程在工做内存中进行相互调用而实现CPU自己对其余CPU、或者说线程对其余线程的内存中资源的访问,而表现这种规则的运行环境通常为运行该程序的运行宿主环境(操做系统、服务器、分布式系统等),而程序自己表现就依赖于编写该程序的语言特性,这里也就是说用Java编写的应用程序在内存管理中的实现就是遵循其部分原则,也就是前边说起到的JMM定义了Java语言针对内存的一些的相关规则。然而,虽然设计之初是为了可以更好支持多线程,可是该模型的应用和实现固然不局限于多处理器,而在JVM编译器编译Java编写的程序的时候以及运行期执行该程序的时候,对于单CPU的系统而言,这种规则也是有效的,这就是是上边提到的线程和线程之间的内存策略。JMM自己在描述过程没有提过具体的内存地址以及在实现该策略中的实现方法是由JVM的哪个环节(编译器、处理器、缓存控制器、其余)提供的机制来实现的,甚至针对一个开发很是熟悉的程序员,也不必定可以了解它内部对于类、对象、方法以及相关内容的一些具体可见的物理结构。相反,JMM定义了一个线程与主存之间的抽象关系,其实从上边的图能够知道,每个线程能够抽象成为一个工做内存(抽象的高速缓存和寄存器),其中存储了Java的一些值,该模型保证了Java里面的属性、方法、字段存在必定的数学特性,按照该特性,该模型存储了对应的一些内容,而且针对这些内容进行了必定的序列化以及存储排序操做,这样使得Java对象在工做内存里面被JVM顺利调用,(固然这是比较抽象的一种解释)既然如此,大多数JMM的规则在实现的时候,必须使得主存和工做内存之间的通讯可以得以保证,并且不能违反内存模型自己的结构,这是语言在设计之处必须考虑到的针对内存的一种设计方法。这里须要知道的一点是,这一切的操做在Java语言里面都是依靠Java语言自身来操做的,由于Java针对开发人员而言,内存的管理在不须要手动操做的状况下自己存在内存的管理策略,这也是Java本身进行内存管理的一种优点。缓存

      [1]原子性(Atomicity):

      这一点说明了该模型定义的规则针对原子级别的内容存在独立的影响,对于模型设计最初,这些规则须要说明的仅仅是最简单的读取和存储单元写入的的一些操做,这种原子级别的包括——实例、静态变量、数组元素,只是在该规则中不包括方法中的局部变量。安全

      [2]可见性(Visibility):

      在该规则的约束下,定义了一个线程在哪一种状况下能够访问另一个线程或者影响另一个线程,从JVM的操做上讲包括了从另一个线程的可见区域读取相关数据以及将数据写入到另一个线程内。服务器

      [3]可排序性(Ordering):

      该规则将会约束任何一个违背了规则调用的线程在操做过程当中的一些顺序,排序问题主要围绕了读取、写入和赋值语句有关的序列。
      若是在该模型内部使用了一致的同步性的时候,这些属性中的每个属性都遵循比较简单的原则:和全部同步的内存块同样,每一个同步块以内的任何变化都具有了原子性以及可见性,和其余同步方法以及同步块遵循一样一致的原则,并且在这样的一个模型内,每一个同步块不能使用同一个锁,在整个程序的调用过程是按照编写的程序指定指令运行的。即便某一个同步块内的处理可能会失效,可是该问题不会影响到其余线程的同步问题,也不会引发连环失效。简单讲:当程序运行的时候使用了一致的同步性的时候,每一个同步块有一个独立的空间以及独立的同步控制器和锁机制,而后对外按照JVM的执行指令进行数据的读写操做。这种状况使得使用内存的过程变得很是严谨!
      若是不使用同步或者说使用同步不一致(这里能够理解为异步,但不必定是异步操做),该程序执行的答案就会变得极其复杂。并且在这样的状况下,该内存模型处理的结果比起大多数程序员所指望的结果而言就变得十分脆弱,甚至比起JVM提供的实现都脆弱不少。由于这样因此出现了Java针对该内存操做的最简单的语言规范来进行必定的习惯限制,排除该状况发生的作法在于:
      JVM线程必须依靠自身来维持对象的可见性以及对象自身应该提供相对应的操做而实现整个内存操做的三个特性,而不是仅仅依靠特定的修改对象状态的线程来完成如此复杂的一个流程。
      *:综上所属,JMM在JVM内部实现的结构就变得相对复杂,固然通常的Java初学者能够不用了解得这么深刻。】**网络

      [4]三个特性的解析(针对JMM内部):

      原子性(Atomicity):

      访问存储单元内的任何类型的字段的值以及对其更新操做的时候,除开long类型和double类型,其余类型的字段是必需要保证其原子性的,这些字段也包括为对象服务的引用。此外,该原子性规则扩展能够延伸到基于long和double的另外两种类型:volatile long和volatile double(volatile为java关键字),没有被volatile声明的long类型以及double类型的字段值虽然不保证其JMM中的原子性,可是是被容许的。针对non-long/non-double的字段在表达式中使用的时候,JMM的原子性有这样一种规则:若是你得到或者初始化该值或某一些值的时候,这些值是由其余线程写入,并且不是从两个或者多个线程产生的数据在同一时间戳混合写入的时候,该字段的原子性在JVM内部是必须获得保证的。也就是说JMM在定义JVM原子性的时候,只要在该规则不违反的条件下,JVM自己不去理睬该数据的值是来自于什么线程,由于这样使得Java语言在并行运算的设计的过程当中针对多线程的原子性设计变得极其简单,并且即便开发人员没有考虑到最终的程序也没有太大的影响。再次解释一下:这里的原子性指的是原子级别的操做,好比最小的一块内存的读写操做,能够理解为Java语言最终编译事后最接近内存的最底层的操做单元,这种读写操做的数据单元不是变量的值,而是本机码,也就是前边在讲《Java基础知识》中提到的由运行器解释的时候生成的Native Code。多线程

      可见性(Visibility):

      当一个线程须要修改另外线程的可见单元的时候必须遵循如下原则:并发

  • 一个写入线程释放的同步锁和紧随其后进行读取的读线程的同步锁是同一个
    从本质上讲,释放锁操做强迫它的隶属线程【释放锁的线程】从工做内存中的写入缓存里面刷新(专业上讲这里不该该是刷新,能够理解为提供)数据(flush操做),而后获取锁操做使得另一个线程【得到锁的线程】直接读取前一个线程可访问域(也就是可见区域)的字段的值。由于该锁内部提供了一个同步方法或者同步块,该同步内容具备线程排他性,这样就使得上边两个操做只能针对单一线程在同步内容内部进行操做,这样就使得全部操做该内容的单一线程具备该同步内容(加锁的同步方法或者同步块)内的线程排他性,这种状况的交替也能够理解为具备“短暂记忆效应”。
    这里须要理解的是同步的双重含义:使用锁机制容许基于高层同步协议进行处理操做,这是最基本的同步;同时系统内存(不少时候这里是指基于机器指令的底层存储关卡memory barrier,前边提到过)在处理同步的时候可以跨线程操做,使得线程和线程之间的数据是同步的。这样的机制也折射出一点,并行编程相对于顺序编程而言,更加相似于分布式编程。后一种同步能够做为JMM机制中的方法在一个线程中运行的效果展现,注意这里不是多个线程运行的效果展现,由于它反应了该线程愿意发送或者接受的双重操做,而且使得它本身的可见区域能够提供给其余线程运行或者更新,从这个角度来看,使用锁和消息传递能够视为相互之间的变量同步,由于相对其余线程而言,它的操做针对其余线程也是对等的。
  • 一旦某个字段被申明为volatile,在任何一个写入线程在工做内存中刷新缓存的以前须要进行进一步的内存操做,也就是说针对这样的字段进行当即刷新,能够理解为这种volatile不会出现通常变量的缓存操做,而读取线程每次必须根据前一个线程的可见域里面从新读取该变量的值,而不是直接读取。
  • 当某个线程第一次去访问某个对象的域的时候,它要么初始化该对象的值,要么从其余写入线程可见域里面去读取该对象的值;这里结合上边理解,在知足某种条件下,该线程对某对象域的值的读取是直接读取,有些时候却须要从新读取。
    这里须要当心一点的是,在并发编程里面,很差的一个实践就是使用一个合法引用去引用不彻底构造的对象,这种状况在从其余写入线程可见域里面进行数据读取的时候发生频率比较高。从编程角度上讲,在构造函数里面开启一个新的线程是有必定的风险的,特别是该类是属于一个可子类化的类的时候。Thread.start由调用线程启动,而后由得到该启动的线程释放锁具备相同的“短暂记忆效应”,若是一个实现了Runnable接口的超类在子类构造子执行以前调用了Thread(this).start()方法,那么就可能使得该对象在线程方法run执行以前并无被彻底初始化,这样就使得一个指向该对象的合法引用去引用了不彻底构造的一个对象。一样的,若是建立一个新的线程T而且启动该线程,而后再使用线程T来建立对象X,这种状况就不能保证X对象里面全部的属性针对线程T都是可见的除非是在全部针对X对象的引用中进行同步处理,或者最好的方法是在T线程启动以前建立对象X。
  • 若一个线程终止,全部的变量值都必须从工做内存中刷到主存,好比,若是一个同步线程由于另外一个使用Thread.join方法的线程而终止,那么该线程的可见域针对那个线程而言其发生的改变以及产生的一些影响是须要保证可知道的。
      注意:若是在同一个线程里面经过方法调用去传一个对象的引用是绝对不会出现上边说起到的可见性问题的。JMM保证全部上边的规定以及关于内存可见性特性的描述——一个特殊的更新、一个特定字段的修改都是某个线程针对其余线程的一个“可见性”的概念,最终它发生的场所在内存模型中Java线程和线程之间,至于这个发生时间能够是一个任意长的时间,可是最终会发生,也就是说,Java内存模型中的可见性的特性主要是针对线程和线程之间使用内存的一种规则和约定,该约定由JMM定义。
      不只仅如此,该模型还容许不一样步的状况下可见性特性。好比针对一个线程提供一个对象或者字段访问域的原始值进行操做,而针对另一个线程提供一个对象或者字段刷新事后的值进行操做。一样也有可能针对一个线程读取一个原始的值以及引用对象的对象内容,针对另一个线程读取一个刷新事后的值或者刷新事后的引用。
      尽管如此,上边的可见性特性分析的一些特征在跨线程操做的时候是有可能失败的,并且不可以避免这些故障发生。这是一个不争的事实,使用同步多线程的代码并不能绝对保证线程安全的行为,只是容许某种规则对其操做进行必定的限制,可是在最新的JVM实现以及最新的Java平台中,即便是多个处理器,经过一些工具进行可见性的测试发现实际上是不多发生故障的。跨线程共享CPU的共享缓存的使用,其缺陷就在于影响了编译器的优化操做,这也体现了强有力的缓存一致性使得硬件的价值有所提高,由于它们之间的关系在线程与线程之间的复杂度变得更高。这种方式使得可见度的自由测试显得更加不切实际,由于这些错误的发生极为罕见,或者说在平台上咱们开发过程当中根本碰不到。在并行程开发中,不使用同步致使失败的缘由也不只仅是对可见度的不良把握致使的,致使其程序失败的缘由是多方面的,包括缓存一致性、内存一致性问题等。
  •   可排序(Ordering):
      可排序规则在线程与线程之间主要有下边两点:
  • 从操做线程的角度看来,若是全部的指令执行都是按照普通顺序进行,那么对于一个顺序运行的程序而言,可排序性也是顺序的
  • 从其余操做线程的角度看来,排序性如同在这个线程中运行在非同步方法中的一个“间谍”,因此任何事情都有可能发生。惟一有用的限制是同步方法和同步块的相对排序,就像操做volatile字段同样,老是保留下来使用
      【:如何理解这里“间谍”的意思,能够这样理解,排序规则在本线程里面遵循了第一条法则,可是对其余线程而言,某个线程自身的排序特性可能使得它不定地访问执行线程的可见域,而使得该线程对自己在执行的线程产生必定的影响。举个例子,A线程须要作三件事情分别是A一、A二、A3,而B是另一个线程具备操做B一、B2,若是把参考定位到B线程,那么对A线程而言,B的操做B一、B2有可能随时会访问到A的可见区域,好比A有一个可见区域a,A1就是把a修改称为1,可是B线程在A线程调用了A1事后,却访问了a而且使用B1或者B2操做使得a发生了改变,变成了2,那么当A按照排序性进行A2操做读取到a的值的时候,读取到的是2而不是1,这样就使得程序最初设计的时候A线程的初衷发生了改变,就是排序被打乱了,那么B线程对A线程而言,其身份就是“间谍”,并且须要注意到一点,B线程的这些操做不会和A之间存在等待关系,那么B线程的这些操做就是异步操做,因此针对执行线程A而言,B的身份就是“非同步方法中的‘间谍’。】
      一样的,这仅仅是一个最低限度的保障性质,在任何给定的程序或者平台,开发中有可能发现更加严格的排序,可是开发人员在设计程序的时候不能依赖这种排序,若是依赖它们会发现测试难度会成指数级递增,并且在复合规定的时候会由于不一样的特性使得JVM的实现由于不符合设计初衷而失败。
      注意:第一点在JLS(Java Language Specification)的全部讨论中也是被采用的,例如算数表达式通常状况都是从上到下、从左到右的顺序,可是这一点须要理解的是,从其余操做线程的角度看来这一点又具备不肯定性,对线程内部而言,其内存模型自己是存在排序性的。【
    :这里讨论的排序是最底层的内存里面执行的时候的NativeCode的排序,不是说按照顺序执行的Java代码具备的有序性质,本文主要分析的是JVM的内存模型,因此但愿读者明白这里指代的讨论单元是内存区。】

      iii.原始JMM缺陷:

      JMM最初设计的时候存在必定的缺陷,这种缺陷虽然现有的JVM平台已经修复,可是这里不得不说起,也是为了读者更加了解JMM的设计思路,这一个小节的概念可能会牵涉到不少更加深刻的知识,若是读者不能读懂没有关系先看了文章后边的章节再返回来看也能够。

      1)问题1:不可变对象不是不可变的

      学过Java的朋友都应该知道Java中的不可变对象,这一点在本文最后讲解String类的时候也会说起,而JMM最初设计的时候,这个问题一直都存在,就是:不可变对象彷佛能够改变它们的值(这种对象的不可变指经过使用final关键字来获得保证),(Publis Service Reminder:让一个对象的全部字段都为final并不必定使得这个对象不可变——全部类型还必须是原始类型而不能是对象的引用。而不可变对象被认为不要求同步的。可是,由于在将内存写方面的更改从一个线程传播到另一个线程的时候存在潜在的延迟,这样就使得有可能存在一种竞态条件,即容许一个线程首先看到不可变对象的一个值,一段时间以后看到的是一个不一样的值。这种状况之前怎么发生的呢?在JDK 1.4中的String实现里,这儿基本有三个重要的决定性字段:对字符数组的引用、长度和描述字符串的开始数组的偏移量。String就是以这样的方式在JDK 1.4中实现的,而不是只有字符数组,所以字符数组能够在多个String和StringBuffer对象之间共享,而不须要在每次建立一个String的时候都拷贝到一个新的字符数组里。假设有下边的代码:
    String s1 = "/usr/tmp";
    String s2 = s1.substring(4); // "/tmp"
      这种状况下,字符串s2将具备大小为4的长度和偏移量,可是它将和s1共享“/usr/tmp”里面的同一字符数组,在String构造函数运行以前,Object的构造函数将用它们默认的值初始化全部的字段,包括决定性的长度和偏移字段。当String构造函数运行的时候,字符串长度和偏移量被设置成所须要的值。可是在旧的内存模型中,由于缺少同步,有可能另外一个线程会临时地看到偏移量字段具备初始默认值0,然后又看到正确的值4,结果是s2的值从“/usr”变成了“/tmp”,这并非咱们真正的初衷,这个问题就是原始JMM的第一个缺陷所在,由于在原始JMM模型里面这是合理并且合法的,JDK 1.4如下的版本都容许这样作。

      2)问题2:从新排序的易失性和非易失性存储

      另外一个主要领域是与volatile字段的内存操做从新排序有关,这个领域中现有的JMM引发了一些比较混乱的结果。现有的JMM代表易失性的读和写是直接和主存打交道的,这样避免了把值存储到寄存器或者绕过处理器特定的缓存,这使得多个线程通常能看见一个给定变量最新的值。但是,结果是这种volatile定义并无最初想象中那样如愿以偿,而且致使了volatile的重大混乱。为了在缺少同步的状况下提供较好的性能,编译器、运行时和缓存一般是容许进行内存的从新排序操做的,只要当前执行的线程分辨不出它们的区别。(这就是within-thread as-if-serial semantics[线程内彷佛是串行]的解释)可是,易失性的读和写是彻底跨线程安排的,编译器或缓存不能在彼此之间从新排序易失性的读和写。遗憾的是,经过参考普通变量的读写,JMM容许易失性的读和写被重排序,这样觉得着开发人员不能使用易失性标志做为操做已经完成的标志。好比:

     

Map configOptions;
char[] configText;
volatile boolean initialized = false;

// 线程1
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText,configOptions);
initialized = true;

// 线程2
while(!initialized)
sleep();

  这里的思想是使用易失性变量initialized担任守卫来代表一套别的操做已经完成了,这是一个很好的思想,可是不能在JMM下工做,由于旧的JMM容许非易失性的写(好比写到configOptions字段,以及写到由configOptions引用Map的字段中)与易失性的写一块儿从新排序,所以另一个线程可能会看到initialized为true,可是对于configOptions字段或它所引用的对象尚未一个一致的或者说当前的针对内存的视图变量,volatile的旧语义只承诺在读和写的变量的可见性,而不承诺其余变量,虽然这种方法更加有效的实现,可是结果会和咱们设计之初截然不同。
相关文章
相关标签/搜索