Java内存模型——深入理解Java虚拟机

 

Java虚拟机规范中试图定义一种Java内存模型来频闭掉各种硬件系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果。

 

 

一.主内存与工作内存

 

Java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机种将变量储存到内存和从内存中取出变量这样的底层细节。

 

Java内存模型:

Java内存模型规定了所有的变量都存储在主内存中,与介绍物理硬件时的内存名字一样,两者可以互相类比。每条线程还有自己的工作内存(可与前面认识到的处理器的高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主存中变量。不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

 

Java内存模型——深入理解Java虚拟机

 

 

 二.内存之间的交互操作

 

关于内存之间具体的交互协议,即一个变量如何从主存拷贝到工作内存,如何从工作内存同步回内主存之类的实现细节,Java内存模型中定义了一下8中操来完成,虚拟机实现按是必须保证下面提及到的每一种操作都是原子的,不可再分的。

 

Lock(锁定):做用于主存的变量,它把一个变量表示为一条线程独占状态。

Unlock(解锁):作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

Read(读取):作用于主内存的变量,它把一个变量的值从主内存传递到线程的工作内存中,以便随后的load动作使用。

Use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值  的字节码指令时将会执行这个操作。

Assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

Store(存储):作用在工作区内存的变量,它把工作内存中一个变量的值传送到主存中,以便随后write操作使用。

Write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量之中。

 

注意事项:

如果要把一个变量从主内存赋值到工作内存,那就要顺序的执行read和load操作,如果要把变量从工作内存同步到主内存,就要顺序地执行store和write操作。注意,Java内存某型只要求上述连个操作必须顺序执行,而没有保证是连续执行,也就是说其之间可以插入其它指令。

 

 

Java内存模型规范还规定了在执行上述八种基本操作时必须满足如下规则

 

  1. 不允许read和load,store和write操作之一单独出现,即不允许一个变量从主存读取了但工作内存不接受,或者从工作内存发起回写但主内存不接受的情况出现。
  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变后必须把该变化同步回主内存。
  3. 不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的的工作内存同步回主内存中。
  4. 一个变量只能在内存中“诞生”,不允许工作内存中直接使用一个未被初始化的变量,换句话说,就是对一个变量实施use,store操作之前,必须执行过assign和load操作。
  5. 一个变量在一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程执行多次。
  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始变量的值。
  7. 如果一个变量事先没有被lock操作锁定,那就不允许去unlocak操作,也不允许去unlock一个被其它线程锁定住的变量。
  8. 对一个变量执行unlock之前,必须先把此变量同步回主内存中。

 

 

 

 

三.对于volatile型别量的特殊规则。

 

关键字volatile可以说是Java虚拟机提供的最轻量的同步机制。

 

volatile的两种特性之可见性:

当一个变量被指定为valatile之后它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量则做不到这一点,普通变量的值在线程间传递均需要通过主内存作为中间介质来完成。

 

由于volatile变量只能满足可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized)来保证原子性。

  1. 运算结果不依赖变量的当前值,或者能够确保只有单一线程修改变量的值。
  2. 变量不需要与其它的状态变量共同参与不变约束。

 

 

volatile的两种特性之禁止指令重排序:

使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅仅能保证该方法的执行过程中所有依赖赋值结果的地方都能获得到正确的结果,而不能保证变量的赋值操作的顺序与程序代码中执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓“线程内表现为穿行的语义”。(这里涉及到一个屏障的概念,就是说在多CPU环境中,所谓的禁止重排序就是指不能把后面的指令重排序到屏障前面)。

 

 

volatlie一定优于synchronized吗?

解决了volatile的语义问题,在来看看在众多保障并发安全的工具中选择volatile的意义——它能让我们的代码比使用其它同步工具更快吗?在某些情况下。volatile的同步机制确实要优于锁,但是由于虚拟机对锁实行的许多消除和优化,使得我们很难量化的认为volatile就会比synchronized快多少。

 

 

 

四.对于long和double型变量的特殊规则。

 

Java内存模型要求lock,unlock,read,load,assign,use,store,write这八个操作都是具有原子性的,但是对于64位的数据类型(long,double)在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写分为两次32位的操作来进行,这就是所谓的long和double的非原子性协定。

 

 

目前的主流虚拟机中都将把对64位数据的非原子性操作实现为具有原子性的操作。所以我们不必要太关注读取到“半个变量”这种情况。

 

 

五.原子性,可见性,有序性。

 

Java内存模型是围绕着在并发过程中如何处理原子性,可见性,有序性这3个特征来建立的。我们逐个来看一下那些操作实现了这3个特性。

 

  • 原子性:由Java内存模型来直接保证的原子性变量操作包括read,load,assign,use, store和write,我们可以大致的认为基本数据类型的访问读写是具备原子性的。如果应 用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和 unlock操作来满足这种需求,尽管虚拟机并没有把lock和unlock操作直接开放给用户 使用,但是确提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这 两个操作,这连个字节码指令反应到Java代码中就是同步块synchronized关键字,因 此在synchronized块之间的操作具有原子性。
  • 可见性是指当一个线程修改了共享变量的值,其他线程都能立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主存,在变量读取前从主存刷新变量值这种 依赖主存作为传递媒介的方式来实现可见性,无论是普通变量还是volatile变量都是如 此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主 内存,以及每次使用前立即从主存内刷新。因此,可以说,volatile保证了多线程操作 时变量的可见性,而普通变量则不能保证这一点。除了volatile之外,final修饰的变量, synchronized块内的变量也能实现可见性。
  • 有序性:Java内存模型的有序性在前面讲解volatile时也详细地讨论过了,Java程序 中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果 在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行 的语义”,后半句是指“指令重排序”现象和“工作内存与主存同步延迟”现象。Java 语言提供了volatile禁止指令重新排序,而synchronized则是由“一个变量在同一个时 刻只允许一个线程对其进行lock操作”,这条规定获得的,这条规则决定了持有同一 个锁的两个同步块只能串行的进入。

 

 

 

六.先行发生原则

 

我们一直在受益于先行发生原则:

 

如果Java内存模型中所有的有序性都仅仅依靠volatile和synchronized来完成,那么有一些操作将变得很繁琐,但是我们在编写Java程序时确没有感觉到这一点,这是因为Java语言中有一个“先行发生”的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。

 

 

天然的先行发生:

下面是Java内存模型中存在的一些“天然的”先行发生关系,这些先行发生关系无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系并不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保证,虚拟机可以对它们随意地进行重排序。

 

 

  • 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支,循环等。
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面是指时间上的先后顺序”。
  • Volatile变量规则:对于一个volatile变量的写操作先行发生于对这个变量的写操作,这里的“后面”同样是指时间上的先后顺序。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则:对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出操作A先行发生于操作C得结论。

 

均为读书笔记,如有差池,还望海涵!