美文网首页
JMM——Java内存模型

JMM——Java内存模型

作者: Harri2012 | 来源:发表于2019-04-03 09:38 被阅读0次

JMM讲什么

内存模型(Memory Model)描述了多个线程之间通过内存交互的规范,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致。在现代的多处理器(多核处理器)系统中,处理器拥有多级缓存以提升内存访问速度同时减少了内存总线的访问量。变量最终会保存在内存中,但是编译器、运行时、处理器可以对指令优化和重新排序,缓存、寄存器也对内存进行了读写优化,只要保证在单个线程内行为与代码顺序串行语义相同即可。内存模型定义了充分且必要的条款,描述了程序中变量之间的关系,以及变量的读取、写入的底层细节,实现了并发过程中的原子性、可见性、有序性。

老版本JMM中的问题

原始的Java内存模型存在一些不足,因此Java内存模型在Java 1.5时被重新修订(JSR133)。这个版本的Java内存模型在Java 8中仍然在使用。老版本中的问题有:

  1. final字段的值并不是完全不变的。构造器中对final字段值的写入可以重排序至构造函数返回并将对象引用赋值给变量之后,导致其它线程看到还未完成初始化的final字段。这个问题的经典案例是String的早期实现中,有多个final字段,但是其它线程可以看到字符串长度为0,而实际上字符串长度并不为0。
  2. volatile字段的写操作与非volatile字段的读写操作重排序。由于重排序的缘故,volatile字段的写操作之前的操作被重新排序至之后进行,导致其它线程看到的结果与程序代码不一致。这个问题的经典案例是Double-Checked Locking(也称multi-threaded singleton pattern)问题,即通过一个volatile字段来判断是否需要进行初始化,从而实现延迟初始化并减少锁操作的性能损耗。由于重排序的问题,导致部分初始化操作或构造操作被排序至volatile字段写操作之后,导致其它线程看到部分初始化的数据,破坏了数据的一致性。

JSR133解决的问题

  1. volatile语义增强:volatile字段的读、写操作与其它任务内存操作操作重排序,volatile的读操作与监视器锁的获取具有相同的内存语义(缓存失效并从主存重新读取),volatile的定操作与监视器锁的释放具有相同的内存语义(缓存刷入主存)。在这个约定下,线程A写入volatile字段V后,线程B可以读出V的值,同时线程A在写入V时能够看到的变量值对线程B也可见。

    是否可以重排序 第二个操作 第二个操作 第二个操作
    第一个操作 普通读/普通写 volatile读/monitor enter volatile写/monitor exit
    普通读/普通写 No
    voaltile读/monitor enter No No No
    volatile写/monitor exit No No

    其中普通读指getfield, getstatic, 非volatile数组的arrayload, 普通写指putfield, putstatic, 非volatile数组的arraystore。volatile读写分别是volatile字段的getfield, getstatic和putfield, putstatic。monitorenter是进入同步块或同步方法,monitorexist指退出同步块或同步方法。

  2. final字段增强:只要对象正确构造,那么不需要使用同步就可以保证任意线程都能看到final字段在构造器中被初始化之后的值。编译器会在final域的写之后,构造器函数return之前,插入StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。写final域的重排序规则可以保证:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。读final域的重排序规则是,禁止处理器重排序初次读取对象引用与初次读取该对象包含的final域这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

    初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此不会重排序这两个操作;大多数处理器也会遵守间接依赖,也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器的。

    对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:

    1. 在构造函数内对一个final引用的对象的成员的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

    2. 确保在final引用的初始化在构造函数内完成,因此一旦其他线程拿到了一个非null的final引用,那么这个引用一定是在构造函数内被正确赋值的(至于是否正确初始化,则不一定,这取决于final引用的赋值语句)

Happens-Before规则

Java内存模型是围绕着如何在并发过程中处理原子性、可见性和有序性这三个特征来构建的,前面已经介绍过volatile关键字的“禁止指令重排序优化”来保证线程之间操作的有序性,另一个我们常用的保证有序性的手段是synchronized关键字;如果Java中的所有有序性都要靠这两个关键字来保证,那代码会很繁琐,但我们平时开发时并没有感觉到这一点,这是因为Java语言中又一个先行发生原则(happens-before)。

HappendBefore规则包括:

  1. 程序次序规则(Program Order Rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。
  2. 监视器锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的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):对线程interrupte()方法的调用优先于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否已中断。
  7. 对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

先行发生原则最容易被误解的地方是,很多人从字面意思上理解这些规则,以“监视器锁定规(Monitor Lock Rule)”为例:

一个unlock操作先行发生于后面对同一个对象锁的lock操作,这句话不是字面意义上,针对同一个锁lock方法必须在unlock方法之后调用(虽然咋看起来没毛病,排他锁确实只能被一个线程lock住),这句话的意思是,当线程A解锁了monitor,接着线程B锁住了该monitor,那么,线程A在解锁之前所有的写操作对线程B而言都是可见的。

相关链接

  1. https://zhuanlan.zhihu.com/p/29881777
  2. http://www.cs.umd.edu/~pugh/java/memoryModel/
  3. https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
  4. http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf
  5. http://gee.cs.oswego.edu/dl/jmm/cookbook.html
  6. https://liuzhengyang.github.io/2017/05/12/javamemorymodel/

相关文章

网友评论

      本文标题:JMM——Java内存模型

      本文链接:https://www.haomeiwen.com/subject/ccfabqtx.html