Java内存模型

java线程之可见性

volatile不需要加锁, 比synchronized更轻量级, 不会阻塞线程; 从内存可见性角度看, volatile读相当于加锁, volatile写相当于解锁。
synchronized既能保证可见性, 又能保证原子性; volatile只能保证可见性,无法保证原子性。

同步

退出同步块 -> 释放监视器 -> 刷新缓冲区到主内存;  进入同步块 -> 获取监视器 -> 本地处理器缓存失效 -> 重新从主内存加载变量。
线程A与线程B之间如要通信: 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

happens-before

从JDK5开始,java使用新的JSR -133内存模型。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

重排序

重排序分为三种: 编译器优化的重排序, 指令级并行的重排序, 内存系统的重排序。

  • 对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。
  • 对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。

内存屏障的四种分类:

屏障类型 指令示例 说明
LoadLoad Barriers Load1; LoadLoad; Load2 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore Barriers Store1; StoreStore; Store2 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore Barriers Load1; LoadStore; Store2 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad Barriers Store1; StoreLoad; Load2 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

数据依赖关系(分为三种: 写后读, 写后写, 读后写)

编译器和处理器在重排序时, 会遵守数据依赖性, 编译器和处理器不会改变存在数据依赖关系的的两个操作的执行顺序, 这里所指的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器和线程间的数据依赖性不被编译器及处理器考虑。

as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

程序顺序规则:如果A happens- before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。如果重排序操作A和操作B后的执行结果与按happens- before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法,JMM允许这种重排序。

顺序一致性

顺讯一致性内存模型是一个被计算机科学家理想化了的理论参考模型。其有两大特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

同步程序的顺序一致性效果:

  • 在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序。JMM会在退出监视器和进入监视器这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。未同步程序在JMM和顺序一致性模型的执行特性有以下差异:

  • 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如同步的多线程程序在临界区内的重排序)。
  • 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
  • JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

volatile

volatile变量特性:

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写-读的内存语义:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

小结: 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

为了实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序, JMM针对编译器制定的volatile重排序规则如下:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

对于x86处理器,仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义 。

锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

公平锁内存语义的实现:

  • 公平锁在释放锁的最后写volatile变量state;在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变的对获取锁的线程可见。

公平锁和非公平锁的区别:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读这个volatile变量。非公平锁获取时,首先会用CAS[即java的compareAndSet()方法调用]更新这个volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

final

基础数据类型

写final域的重排序规则:

  • JMM禁止编译器把final域的写重排序到构造函数之外。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

读final域的重排序规则:

  • 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

引用类型

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

在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

由于x86处理器不会对写-写操作和存在间接依赖关系的操作做重排序, 在x86处理器中,final域的读/写不会插入任何内存屏障!

小结

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。处理器内存模型和语言内存模型都比顺序一致性内存模型要弱。

JMM的设计:

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM允许这种重排序)。

JMM的内存可见性保证:

  • 单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
  • 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
  • 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。

JSR-133对旧内存模型的修补:

  • 增强volatile的内存语义。旧内存模型允许volatile变量与普通变量重排序。JSR-133严格限制volatile变量与普通变量的重排序,使volatile的写-读和锁的释放-获取具有相同的内存语义。
  • 增强final的内存语义。在旧内存模型中,多次读取同一个final变量的值可能会不相同。为此,JSR-133为final增加了两个重排序规则。现在,final具有了初始化安全性。

参考资料

HOllis<java成神之路系列-基础篇> Java内存模型

posted @ 2018-06-07 00:31  qingshanli  阅读(525)  评论(0编辑  收藏  举报