白话说Java虚拟机原理系列【第七章】:JMM内存模型详解



前导说明:
本文基于《深入理解Java虚拟机》第二版和个人理解完成,
以大白话的形式期望能用大家都看懂的描述讲清楚虚拟机内幕,
后续会增加基于《深入理解Java虚拟机》第三版内容,并进行二个版本的对比

背景

1.多线程:由于CPU运算速度远快于存储、通信速度,即远快于I/O(网络通信、数据库访问、磁盘I/O访问等),为了不让CPU执行完后浪费大量时间去等待I/O的执行,所以我们有了让CPU同时去处理多件事的想法,也即多线程运行。

2.并发:紧接着让一个服务器同时对多个客户端提供服务的点子也出现了,这则是另一个能充分利用CPU的想法,且衡量一个服务器的好坏高低,每秒事务处理数(Transactions Per Second,TPS )是最重要的指标之一,它代表着一秒内服务器平均能响应的总请求数。

3.缓存的出现:因为无论什么程序,即便是多线程运行,都不可能是单单由CPU自己能完成的,它总是会需要跟内存交互(因为即便是运算也要有被运算的数啊,来源当然就是从内存获得了,当然运算结束还要将数据写回内存),但是内存的性能又比较低下,所以就引入了高速高性能的缓存,如CPU的一级、二级、三级缓存。通过让CPU跟缓存交互,然后缓存预先把数据从内存中拷贝过来。CPU与缓存交互完后再将结果写回到内存中。通过下图可知,其实缓存是在CPU内部的,并且细化到核心的话其实是一个核心一个缓存,并且一级、二级缓存是cpu内核独享的,三级缓存才是所有cpu内核共享的,当然距离核心越近缓存速度就越快。
在这里插入图片描述
注意:这里是操作系统模型的示意图,而JVM运行在操作系统之上,虽然JVM的内存分配上和操作系统的概念不大一样,但JVM始终是通过与操作系统交互来调度底层的这些硬件能力的,所以我们先从操作系统层面进行了解

4.缓存一致性问题:通过CPU缓存我们会发现一个问题,就是一旦运行多线程,那么比如一个核心运行一个线程,如果要同时操作内存中的一个数据,那么流程就是:
三级缓存拷贝内存数据到自己区域,然后是拷贝到二级缓存,再到一级缓存,即每个缓存都会有此数据的备份,最后cpu核心与一级缓存交互,交互完成后最后在一级缓生成交互结果,然后再将结果写入二级缓存,再由二级缓存写入到三级缓存,最终写入到内存,那么问题来了,一级缓存、二级缓存倒是没有问题,因为是cpu核心独享,但是到了向三级缓存和内存写入操作完的新数据时,因为是多线程,比如有2个核心都会将结果写入,以哪个为准呢?这就会出现一方覆盖另一方的问题,就会出现有一方获取的数据可能是错误的,提现出来的问题就是双方对各自的结果都不可见。这就是多线程引发的缓存一致性问题,也即因为共享变量的存在,所以引发了缓存一致性的问题。如下图:
在这里插入图片描述
5.CPU乱序执行问题:为了能让CPU的运算单元能被充分利用,除了增加高速缓存外,CPU可能会对输入的代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保障最终的结果与顺序执行的结果保持一致,但是并不能保障程序中各个语句的执行顺序与代码的编写顺序一致,因此如果在代码中存在一个计算任务依赖另一个计算任务的结果,那么不能通过代码的编写顺序来保障,因为可能乱序执行了,而这种存在依赖的代码一旦乱序执行将会出问题。那么怎么保障他们的最终结果是正确的呢?

6.JVM中同样的问题:从操作系统层面讲,引入以上内存模型,就会存在缓存一致性问题和乱序执行问题,同样的,借助操作系统环境的JVM通过JIT即时编译器运行时也会有这种操作,除了存在共享变量缓存一致性问题也会有乱序执行,只不过他的乱序执行叫做指令重排序,那么怎么才能解决这些问题呢?

多线程|并发遇到的问题

以上的内存模型,引出的问题无非就是多线程/并发运行时的问题,如果解决了缓存一致性、指令重排序,那么多线程并发运行就没问题了,即没有线程安全问题了。

操作系统解决方案

解决方案:解决系统层面(缓存一致性/乱序执行),两种方式

  • ①通过对前端总线加锁(加LOCK)的方式
    前端总线,又叫总线,他是一个类似标准的东西,作用就是用来传输数据、通信等。CPU、主板、内存等硬件几乎都有自己的前端总线,而CPU的前端总线主要决定了一个机器能够达到的传输速度快慢,当然CPU的前端总线值越高,传输就越快,CPU通过前端总线与主存、北桥等设备进行数据交互、通信。CPU与主存的数据传输,摆脱不了主板作为媒介,比如CPU的总线值为1333,而主板的总线值为1066,内存的总线为1333,那么整个传输性能只能达到总线为1066的性能,因为主板拖了后腿,所以总线这个概念就类似一个管道,每个设备都有自己的一个管道,要进行通信就需要将管道彼此连接,连接后通过管道来进行传递信息,当然传递信息的快慢是由最细的那个管道决定的。所以如果将总线加锁,那么只能有一个CPU可以进行数据通信和操作,其他CPU只能阻塞等待,所以就不会有缓存一致性问题了,但是这样的代价太大,性能的影响很难接收,所以一般不采用此方案。

  • 通过使用缓存一致性协议的方式
    缓存一致性协议,就是各个CPU的核心去访问缓存(或说是访问缓存与主存时,两者的数据通讯方式)时都遵循这个协议,在读、写时根据协议来进行操作,这样就可以解决缓存一致性问题。当然乱序执行在协议中也有所规范,这些协议的实现都是由操作系统来完成的,所以我们使用操作系统时就没有这些问题了。
    相应的JVM中的缓存一致性、指令重排序问题,就需要我们自己写程序时按照JVM的协议标准开发程序,以此来解决JVM中的缓存一致性和指令重排序问题,其实就是通过我们开发的程序中的指令,底层去访问缓存一致性协议,通过缓存一致性协议来完成问题的解决。

    系统层面的这些协议(缓存一致性协议)包括如下:

    • 1.MSI
    • 2.MESI
      这个是目前使用最多的协议,针对缓存一致性这一块,它确保每个缓存中使用的共享变量的副本是一致的。核心思想:当某个CPU在写数据时,写完后会立即更新数据到主存,并且如果它发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
    • 3.MOSI
    • 4.Synapse
    • 5.Firefly
    • 6.Dragon Protocol 等等

    协议很多,只要操作系统实现其中一种协议即可,按协议操作,就解决了缓存一致性、乱序执行问题。
    在这里插入图片描述

JVM解决方案
基本概念与背景

其实JVM虚拟机中解决了这个问题就解决了Java中存在的并发问题,即线程安全,这也是JVM定义内存模型的作用,所以下边我们学习内存模型到底规定了什么,我们要利用这些规则去开发程序,就会避免这些问题的出现,也就是我们的多线程、并发就是健壮的了。接下来带着这些疑问来看看JVM定义的内存模型到底怎么解决的缓存一致性问题指令重排序问题。

概念:Java虚拟机规范中试图定义一种java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,以此来解决缓存一致性和指令重排序等类似存在风险的问题。

1.主内存: 可以类比成是堆区域的一部分,他跟内存结构的划分不是同一层次,不过为了好理解可以认为是堆的一部分,或干脆认为是堆和方法区,主要就是存储在主内存的数据是线程共享的。
2.工作内存: 可以类比认为是寄存器、Java栈、本地方法栈,很明显存储在此的数据是线程独享的。
3.两者的关系:
类比系统层面缓存一致性那样,线程运行时独享工作内存,共享主内存,线程只能操作工作内存不能直接操作主内存,工作内存会保存主内存中变量的拷贝副本(这个不是直接拷贝,如主内存有个10M变量,就把10M变量拷贝到工作内存?不是的,这个可以认为是一个特殊的标记处理,具体细节书中没说),即需要将主内存的数据拷贝到工作内存,然后再进行操作运算,执行完后需要将工作内存的结果同步到主内存。不同线程之间不能直接访问对方的工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
在这里插入图片描述
所以有了工作内存和主内存模型,我们就知道了JVM的缓存一致性和指令重排序,主要出在这里,换句话说就是需要解决两个问题:
①拷贝变量从主内存到工作内存
②同步变量结果从工作内存到主内存

JVM内存模型提供的能力

1.8种原子操作:JVM规范提供的用来完成线程从主内存拷贝变量到工作内存,再由工作内存同步到主内存。这些操作是由JVM实现完成的,并由JVM保障每一个操作都是原子性的、不可再分的。

  • 1.lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • 2.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • 3.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • 4.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • 5.use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。【就是线程要使用变量了,会执行次命令】
  • 6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。【就是线程要为工作内存的变量赋值了,会执行此命令】
  • 7.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • 8.write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

从功能描述看read和load要一起起作用才能完成一个操作,就是说read是作用于主内存,可认为是主内存发起的动作,load是作用于工作内存,可认为是工作内存发起的动作,也就是从主内存拷贝数据到工作内存,需要主内存发起通知,并且由工作内存发出接受,这两个命令才能完成一个拷贝动作。store、write也是同样的意思,注意理解。

如何保障每个命令是原子操作的呢?
这个是由JVM虚拟机底层字节码指令来保障,因为每个命令如果不能保障原子性,就有可能出现命令中执行了一半被其他指令干扰操作,就会出现问题(更细粒度的一致性问题),这个简单知道即可,但要记住这8个操作都是原子操作。

问题分析: 从8个原子操作上看我们知道,如read和load是分开执行的,所以就有可能不是连续执行的,就是可能会介入其他命令,比如a、b两个变量做拷贝操作,就可能是read a、read b、load b、load a,同样store和write命令也是如此。如果把这个问题看成是原子性或顺序性,那么如果read和load组成原子操作,就不会有其他命令会介入,如果说是顺序性问题呢,如果规定read后边必须紧跟load,那么就不会出现read a和read b挨着了,为了避免类似的问题发生,JVM内存模型规范又规定了8种规则。

2.8项规则:执行以上8种原子操作时需要满足的规则。

  • 1.不允许read和load、store和write操作之一单独出现,
    即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 2.不允许一个线程丢弃它的最近的assign(赋值)操作,
    即变量在工作内存中变化了之后必须把该变化同步回主内存。
    (注意:这里只是说不能作废修改,没有说修改后要立即就同步回主内存,后边的volatile就会要求修改完后必须立即同步回主内存)
  • 3.不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 4.一个新的变量只能在主内存中"诞生",不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,
    换句话说,就是对一个变量实施use、store操作之前,必须先执行了load和assign操作。(个人理解:就是必须先load才能use、必须先assign才能store)。
  • 5.一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同的unlock操作,变量才会被解锁。(实际代码中通过synchronized关键字实现,很明显他是可以重入的,并且该关键词是通过monitorenter和monitorexit字节码指令实现的lock和unlock操作)
  • 6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。(保障了synchronized锁住的变量不会在其他线程中出现,所以不会有缓存一致性问题)
  • 7.如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
  • 8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。(保障解除锁定后其他线程能得到最新的数据)

总结:对以上8个规则简单描述

  • 1.规定read和load或store和write要一起出现。
  • 2.规定工作内存的变量如果assign后必须最终同步到主内存,如果没有操作,不能同步回主内存。
  • 3.新变量只能从主内存开始,不能在工作内存使用一个没有被初始化的变量。
  • 4.关于lock和unlock的规定8项规则的5、6、7、8已经说的很清楚了,这里就不重复赘述了。

3.对于volatile修饰的变量的特殊规则:我们先看如下2个结论

  • 1.被volatile修饰的变量被操作后会立即同步结果到主内存。
    被volatile修饰的变量在工作内存中如果assign(赋值)操作后,JVM会立马要求执行store、write命令,将被volatile修饰的变量的值同步到主内存中,以此保障只要有线程修改volatile变量,就会立即同步到主内存,保障其他线程得到的是最新数据。而不被volatile修饰的普通变量则不会有这种"立即"同步的规则,即不能保障其他线程立即可见,也可以说是解决主内存和工作内存的可见性问题。

    **问题:**只有上边的规则就够了吗?
    比如两个线程同时读取a变量,那么一个线程更改完,虽然立即刷新到主内存,保障了其他线程的可见性,但是另一个线程的工作内存中的数据不还是不对的吗?因为是更新到主内存前读取到工作内存的啊,所以还需要另一个规定,如下。
  • 2.被volatile修饰的变量被使用(use)前会先立即从主内存刷新变量值到工作内存。
    被volatile修饰的变量,再被读入到工作内存后,每次使用(即use)时JVM会自动先执行刷新数据(即从主内存中重新读取),以此来保障执行引擎操作的变量的准确性。正是由于该规则的存在,即便volatile修饰的变量在多个线程中的值是不一致的(缓存一致性问题),由于在是使用时会重新立即从主内存中进行重新读取,所以执行引擎(线程)是来不及发现不一致的情况的,所以就不存在缓存一致性问题。

归纳总结:java内存模型怎么用来支撑以上2个结论是成立的呢?
书中对volatile指定了特殊的3个规则,如下
在这里插入图片描述
原文看上去可能不好理解,我们汇总分析一下,内容如下:

  • ①线程执行use方法时,必须先执行load,而我们在讲8个规则中指定,load时必须read,
    所以==>use、load、read,必须是一体的,顺序不能变
    以此保障某个线程在使用volatile变量时一定是最新的,而不会出现缓存一致性问题
  • ②线程执行assign方法时,必须紧接着执行store,而我们在讲8个规则中指定,store要和write连续,
    所以==>assign、store、write,必须是一体的,顺序不能变
    以此保障某线程改变volatile的数据后立即会更新到主内存
  • ③第三点是要说volatile变量被线程修改后,会设置一个lock锁屏障,以便于volatile变量被修改前后的代码不会被指令重排序干扰

原理分析: 再得出以上结论后,我们在深入了解下,JVM底层是怎么实现的这样的能力呢?即了解一下volatile关键字的实现原理(CPU指令级原理)

  • 1.回顾一下,操作系统的缓存一致性,我们知道有总线锁和MESI协议两种实现方式,而基本新一代的系统都是采用的MESI缓存一致性协议来实现的。
  • 2.通过将字节码指令进行汇编反编译,我们可以知道被volatile修饰的变量,在编译成指令后,会多一个lock关键字。而这个关键字也就是volatile的特殊之处了。说白了,就是因为多了这么一个lock关键字,才让volatile修饰的变量能够具有可见性+禁止指令重排序。
  • 3.这样看上去就简单了,直接看看lock的实现原理就行了呗?
    其实我们知道,虽然由jvm来运行字节码文件,但最终字节码指令都会转换成cpu能识别的指令,且数据也会在cpu的缓存(一级、二级等)和主存之间传递,只不过jvm为了透明化,引入了内存结构和内存模型等概念,让我们只关注jvm而无需关注计算机操作系统底层的指令,而这个lock关键字的作用,其实就是调用缓存一致性协议(MESI协议),使用该协议来完成对lock标记的变量进行缓存与主存的交互,而从jvm的角度分析,就是线程的工作内存中的volatile修饰的变量与主内存中的变量之间的交互规则。再直白点说,这个lock的作用就是借助了MESI协议的功能,它会让lock标记的变量,当有线程修改工作线程中的变量时,它会在修改后立即同步到主内存,并且同时告知其他拥有此变量的工作线程,让他们的工作内存中的此变量失效,这样一来,当其他线程要在工作内存中操作此变量时,会先检查,如果发现已经失效,则此时就会再重新从主存中获取此变量的值,这样就得到了最新的数据了,即解决了缓存一致性问题。所以很明显,这就解释了volatile修饰的变量具有可见性;当然因为lock的功能,他也自动拥有了禁止lock标记的变量之前和之后的指令代码的重排序,因为执行完lock行的代码后,会强制将数据更新到主存,说明lock之前的代码已经执行完了,所以肯定不会和变量后边的代码重排序了,所以就理所当然的禁止了指令重排序,相当于在volatile修饰的变量处加了一道"内存屏障"(Memory Barrier或Memory Fence)。

注意:不保障原子性
我们继续分析volatile的lock关键字语义,我们知道lock是将volatile修饰的变量的操作进行立即刷新到主内存中的操作,并且会让其他线程工作内存中的变量失效,可见性和禁止指令重排序很容易从中理解到,下边我们来从中分析下原子性。通过例子说明比较好理解。

举例:
volatile int i = 1;
i=2;  // ① 
i++;  // ②
  • ① i=2;
    针对i变量,我们发现编译后的指令前会加有lock关键字,而对于变量操作的程序i=2,这就是线程运行时需要对变量进行的操作,此i=2编译后的指令为一行代码,所以运行此代码是按照lock的语义,就是变更后直接同步到主内存,并让其他线程工作内存的i变量的值失效,这样是满足可见性、禁止重排序,并且这个i=2因为是一行代码,所以满足了原子性。
  • ② i++;
    以下为反编译后的字节码指令: 
    getstatic
    iconst_1
    iadd
    putstatic
    
    同上原理,i++这条语句编译后的指令则不是一条,而是4条(如上),但只有其中对i变量修改时是加了lock关键字的,如上代码中的4条指令。从指令我们很容易看出,getstatic命令会保障得到的是正确的。比如三个线程都做i++,A完成putstatic后刷回主内存,并让其他两个线程工作内存中此变量失效,然后其他两个都进行getstatic操作,并操作前检查是否失效,没有失效两个都继续执行,当B执行putstatic后,同样同步回主内存并作废其他,但是因为C已经getstatic检查过了,所以不会被作废,所以C执行putstatic后将会覆盖B的,所以通过lock语义,无法保障多于1条指令的操作的原子性。
  • 3.推论:所以volatile修饰的变量,如果是赋值操作,即i=2这种单指令操作是可以满足线程安全的,即满足可见性、禁止指令重排序和原子性,但如果对volatile修饰的变量做复杂操作,不是原子性操作时,volatile关键字是无法保障它的线程安全的。

问题:经过本节的说明,如果volatile修饰的变量保障操作是原子性,那么就是线程安全的,那么怎么确定是不是原子操作呢?

内存模型也给出了2个规则,用来建议对volatile修饰的变量的操作方式:

  • 1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    意思就是运算的时候不用volatile修饰的变量的当前值来计算,因为如果用了就不是原子操作了,就可能被其他线程改掉了当前值,这样最终就不对了。或者你能保障volatile变量的当前值不会被其他线程修改也可以保障最后的结果正确。比如:i++,就是i=i+1,就是个依赖于当前值运算的例子。
  • 2.变量不需要与其他的状态变量共同参与不变约束。
    其实跟第1条大同小异,这个说的是volatile变量a如果和另一个变量b(不论是volatile变量还是普通变量),维护着一个公式a>b(就是不变式的意思),但是a和b都是变量,就肯定有为a和b单独赋值的方法,即便a和b赋值都是原子操作,也不能保障无时无刻a永远>b,因为如果一个操作是a、b同时赋值,虽然单独的a和b都是线程安全的,但是a、b赋值整体还是个非原子操作,就是a赋值后,其他线程就去调用a>b去了,那么因为b赋值还没运行完,所以a>b就不成立了。

其他用法:当然了volatile不能保障原子性,可以借助synchronized关键字来弥补这个缺陷,这要看实际的应用环境。

volatile修饰变量的性能:由于volatile修饰的变量写操作时会添加类似于"内存屏障"的lock关键字,所以性能肯定要比普通变量差一些,但是肯定要比synchronized或java.util.concurrent包里面的锁要快一些,因为他并非真正加锁,而是通过缓存一致性协议来实现的可见性、禁止指令重排序功能。

并发过程中的三大特性(线程安全问题)

以上我们提到了缓存一致性,而它的问题可以归纳是可见性和原子性问题,而指令重排序又可以归纳为有序性,那换一个角度去说,就是解决多线程并发问题,其实就要解决原子性、可见性、有序性三个问题即可,所以他们也被称为多线程并发问题的三大特性。

其实内存模型一直在围绕着解决并发中的这三大特性来展开的,下边我们看看内存模型中的哪些规则是与这三大特性相对应的,也就是说哪些规则是用来解决哪些特性的,他们是怎么对号入座的呢?

  • 1.原子性
    由Java内存模型规则产生的直接能保障原子性变量的操作包括read、load、use、assign、store和write,另外lock、unlock操作虽然也是原子性操作,不过他们可以组合起来保障他们框起来的代码的原子性。lock和unlock我们不能直接操作,但是我们可以通过实现了字节码指令monitorenter和monitorexit的synchronized关键字来操作,也即在synchronized块中的代码具有原子性。
  • 2.可见性
    • 1.volatile:保障可见性。
    • 2.synchronized:通过lock和unlock的规则得以实现可见性。
    • 3.final:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把"this"引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到"初始化了一半"的对象),那在其他线程中就能看见final字段的值。所以被final修饰的变量也能保障可见性。解释一下,final之所能保障可见性,可以从根源解释,因为它修饰的变量是不可变的,试问一个不可变的变量,怎么可能被修改或者在多个线程的工作内存中出现不一样的情况呢?肯定都是一样的,因为是不可变的。所以只要他被创建完成,就能确定任何地方他出现的位置的值都是一样的,所以也就保障可见了。
  • 有序性
    • 1.volatile:通过禁止指令重排序,可以限定对volatile变量进行操作的代码之后的代码不能重排序到操作volatile变量的代码的前面。
    • 2.synchronized:还是通过lock和unlock来实现的有序性,即一个synchronized代码块中的代码同一时刻只能被同一个线程访问,他是让整个synchronized代码块中的内容和其他代码不去重排序,但是不限制synchronized代码块内部的代码重排序,所以如果想限制内部代码重排序,还需要借助volatile细粒度的禁止重排序的功能,比如:双重检查单例的案例

总结:经过以上我们可以总结出

  • final可以保障可见性
  • volatile可以保障可见性、顺序性
  • synchronized可以保障可见性、顺序性、原子性
先行发生原则(happends-before)

由于内存模型规则太过复杂不好实践,故增加此等效判断原则,其实这个我觉得是三大特性中顺序性的扩充,用来判断一段代码是否是有序的,如果不满足此规则,则JIT编译器可以任意的对代码进行指令重排序。这里可能是巧合?也是8个规则

  • 1.程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 2.管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,这里必须强调的是同一个锁,而"后面"是指时间上的先后顺序。
  • 3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的"后面"同样是指时间上的先后顺序。

    结合前边的volatile规则,在操作工作内存中的volatile变量是会先重新读取主内存的新数据的规则,就更容易理解了,这里说的读肯定是在写完数据后,所以更能确定工作内存中对volatile变量的操作肯定是先得到的最新数据了。注意理解。这个保障的是对volatile变量的可见性,但是不是保障原子性的原则,注意区分。
  • 4.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检查,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 7.对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 8.对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

以上就是JMM内存模型的全部内容,大家可以前后内容结合去理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

上树的蜗牛儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值