Java内存模型(JMM):一个可能知道但没具体了解的概念

写个文章是由于一次字节面试中,问到java内存模型了解吗?我答了一些堆、方法区、虚拟机栈什么的。而后说这个不是。我一脸蒙蔽。。。以后了解到JMM,才知道本身有多蠢,原来是这些东西,原来这些叫JMM。java

因此,如今写一篇文章总结一下。面试

1. Java内存模型的概念

你们都知道java是经过java虚拟机来跨平台运行。但,它是怎么实现的呢,有没有什么规则?数组

:不一样计算机操做系统对内存模型操做不同,这时候就要有统一的规范来完成操做。因此就要经过JAVA内存模型(Java Memory Model,JMM缓存

  • 它是一种JAVA虚拟机规范
  • 它屏蔽各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。

!!!下面2,3,4,5,6段落都是有关JAVA内存模型的相关规范或者规则。!!!安全

文章最后作总结多线程

2. 内存规范:主内存和工做内存

Java内存模型的主要目的是定义程序中各类变量的访问规则并发

  • 关注在虚拟机中把变量值存储到内存从内存中取出变量值这样的底层细节。

这个段落的目标:针对的是线程之间能够共享的变量app

变量根据是否能够共享划分为:线程私有的和线程公有的。函数

  • 线程私有:局部变量与方法参数
  • 线程公有:实例字段、静态字段和构成数组对象的元素

Java内存模型规定了全部的变量都存储在主内存中。每条线程还有本身的工做内存,线程的工做内存中保存了被该线程使用的变量的主内存副本,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存中的数据。如图所示:性能

这一部分要和JAVA内存区域做区分。

  • 内存区域分为:虚拟机栈、本地方法栈、程序计数器(加粗为线程之间隔离的)、方法区、堆区、直接内存

3. 内存操做

主内存与工做内存之间具体的交互协议,

  • 即一个变量如何从主内存拷贝到工做内存、如何从工做内存同步回主内存这一类的实现细节。

Java内存模型中定义了如下8种操做来完成。Java虚拟机实现时必须保证下面说起的每一种操做都是原子的、不可再分的。(简单看看就行

  • lock(锁定):做用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。
  • read(读取):做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用。
  • load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
  • use(使用):做用于工做内存的变量,它把工做内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要使用变量的值的字节码指令时将会执行这个操做。
  • assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
  • store(存储):做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存中,以便随后的write操做使用。
  • write(写入):做用于主内存的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。

Java设计团队,将Java内存模型的操做简化为read、write、lock和unlock四种,但这只是语言描述上的等价化简,Java内存模型的基础设计并未改变。

4. volatile型变量的规则

volatile定义的变量有两个特性:

  • 保证此变量对全部线程的可见性
  • 禁止指令重排序优化

4.1 可见性

1. 概念:当一条线程修改了这个变量的值,新值对于其余线程来讲是能够当即得知的。而普通变量并不能作到这一点,普通变量的值在线程间传递时均须要经过主内存来完成

  • 普通变量写入读取流程:线程A修改一个普通变量的值,而后向主内存进行回写,另一条线程B在线程A回写完成了以后再对主内存进行读取操做,新变量值才会对线程B可见。

2. 线程安全

在并发中,并不必定是线程安全。

Java里面的运算操做(这里指的是a=b+1,相似这种,不是a=1这样)符并不是原子操做,这致使volatile变量的运算在并发下同样是不安全的。

网上有不少利用线程对一个变量10000次,可是最后结果不是10000*线程数

变量的++操做在字节码中分解为三个部分(此处并不严谨,表明意思为分红多步骤),这样会致使线程不安全(单独的读写是安全的)。

  • 读值
  • 改值
  • 存值

4.2 禁止指令重排序

1. 概念:是指处理器采用了容许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并非说指令任意重排,处理器必须能正确处理指令依赖状况保障程序能得出正确的执行结果。

注意:在同一个线程的方法执行过程当中没法感知到指令重排序,可是其实其中的一些执行顺序发生了改变但保证结果不变

  • 由于Java内存模型中定义“线程内表现为串行的语义”。

2. 禁止指令重排序的例子:单例模式懒汉

class Singleton{
  private static volatile Singleton instance = null;
  private Singleton(){}
  public static Singleton getInstance(){
    if(instance == null){
      synchronized(Singleton.class){
        if(instance == null){
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}
复制代码

代码解释:假如没有volatile修饰,在new Singleton的时候,对instance已经赋予了内存空间,可是内存中没有东西。此时有另外一个线程获取单例去使用,发现这个内存中没有对象没法使用(就是初始化一半),发生了线程安全的问题。

原理解释:汇编指令中增长lock修饰

  • 它将本处理器的缓存写入了内存,该写入动做也会引发别的处理器或者别的内核无效化其缓存。
  • 它至关于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障以前的位置,注意不要与第3章中介绍的垃圾收集器用于捕获变量访问的内存屏障互相混淆)

3. 使用原则

  • volatile变量读操做的性能消耗与普通变量几乎没有什么差异,可是写操做则可能会慢上一些,由于它须要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即使如此,大多数场景下volatile的总开销仍然要比锁来得更低
  • 咱们在volatile与锁中选择的惟一判断依据仅仅是volatile的语义可否知足使用场景的需求。

4.3 总结

  1. 要求在工做内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其余线程对变量V所作的修改。
  2. 要求在工做内存中,每次修改V后都必须马上同步回主内存中,用于保证其余线程能够看到本身对变量V所作的修改。
  3. 要求volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同

5. long和double的特殊规则

对于上面的Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操做都具备原子性。

可是对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:容许虚拟机将没有被volatile修饰的64位数据的读写操做划分为两次32位的操做来进行,

  • 容许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操做的原子性,这就是所谓的“long和double的非原子性协定”

通过实际测试,在目前主流平台下商用的64位Java虚拟机中并不会出现非原子性访问行为,可是对于32位的Java虚拟机,譬如比较经常使用的32位x86平台下的HotSpot虚拟机,对long类型的数据确实存在非原子性访问的风险。

  • 编写代码时通常不须要由于这个缘由刻意把用到的long和double变量专门声明为volatile。

6. happens-before原则

若是Java内存模型中全部的有序性都仅靠volatile和synchronized来完成,那么有不少操做都将会变得很是啰嗦,可是咱们在编写Java并发代码的时候并无察觉到这一点,这是由于Java语言中有一个“先行发生”(Happens-Before)的原则。

Java内存模型下一些自然的先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,能够在编码中直接使用。存在8中规则:

  1. 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操做先行发生于书写在后面的操做。注意,这里说的是控制流顺序而不是程序代码顺序,由于要考虑分支、循环等结构。
  2. 管程锁定规则(Monitor Lock Rule):一个unlock操做先行发生于后面对同一个锁的lock操做。这里必须强调的是“同一个锁”,而“后面”是指时间上的前后。
  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操做先行发生于后面对这个变量的读操做,这里的“后面”一样是指时间上的前后。
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个动做。
  5. 线程终止规则(Thread Termination Rule):线程中的全部操做都先行发生于对此线程的终止检测,咱们能够经过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过Thread::interrupted()方法检测到是否有中断发生。
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  8. 传递性(Transitivity):若是操做A先行发生于操做B,操做B先行发生于操做C,那就能够得出操做A先行发生于操做C的结论。

7.总结

7.1 原子性

由Java内存模型来直接保证的原子性变量操做包括read、load、assign、use、store和write这六个,咱们大体能够认为,基本数据类型的访问、读写都是具有原子性的。

  • 例外就是long和double的非原子性协定,读者只要知道这件事情就能够了,无须太过在乎这些几乎不会发生的例外状况。

若是应用场景须要一个更大范围的原子性保证,Java内存模型还提供synchronized关键字,所以在synchronized块之间的操做也具有原子性。

  • 经过lock和unlock操做,但虚拟机未把lock和unlock操做直接开放给用户使用,可是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操做。

7.2 可见性

可见性就是指当一个线程修改了共享变量的值时,其余线程可以当即得知这个修改。

  • volatile的可见性是,volatile的特殊规则保证了新值能当即同步到主内存,以及每次使用前当即从主内存刷新。所以咱们能够说volatile保证了多线程操做时变量的可见性,而普通变量则不能保证这一点。
  • synchronized同步块的可见性是由“对一个变量执行unlock操做以前,必须先把此变量同步回主内存中(执行store、write操做)”这条规则得到的。
  • final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,而且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其余线程有可能经过这个引用访问到“初始化了一半”的对象),那么在其余线程中就能看见final字段的值。

7.3 有序性

Java程序中自然的有序性能够总结为:

  • 若是在本线程内观察,全部的操做都是有序的;
  • 若是在一个线程中观察另外一个线程,全部的操做都是无序的。

前半句是指线程内似表现为串行的语义,后半句是指指令重排序现象和工做内存与主内存同步延迟现象。

  • volatile关键字自己就包含了禁止指令重排序的语义。
  • synchronized则是由一个变量在同一个时刻只容许一条线程对其进行lock操做这条规则得到的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

这篇文章参考:《深刻理解Java虚拟机(第3版)》

相关文章
相关标签/搜索