Java并发编程(十四)Java内存模型

1.共享内存和消息传递

  线程之间的通讯机制有两种:共享内存和消息传递;在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间经过写-读内存中的公共状态来隐式进行通讯。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须经过明确的发送消息来显式进行通讯。
同步是指程序用于控制不一样线程之间操做发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。工程师必须显式指定某个方法或某段代码须要在线程之间互斥执行。在消息传递的并发模型里,因为消息的发送必须在消息的接收以前,所以同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通讯老是隐式进行,整个通讯过程对工程师彻底透明。java

 

2.Java内存模型的抽象
  在java中,全部实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通讯由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。Java内存模型的抽象示意图以下:程序员

从上图来看,线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:编程

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 线程B到主内存中去读取线程A以前已更新过的共享变量。

 

3.从源代码到指令序列的重排序数组

在执行程序时为了提升性能,编译器和处理器经常会对指令作重排序。重排序分三种类型:缓存

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。 

从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:多线程

上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序均可能会致使多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是全部的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,经过内存屏障指令来禁止特定类型的处理器重排序(不是全部的处理器重排序都要禁止)。 
JMM属于语言级的内存模型,它确保在不一样的编译器和不一样的处理器平台之上,经过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。并发

 

4.happens-before简介
happens-before是JMM最核心的概念,对于Java工程师来讲,理解happens-before是理解JMM的关键。app

JMM的设计意图编程语言

在设计JMM须要考虑两个关键因素:ide

  1. 工程师对内存模型的使用,但愿内存模型易于理解和编程,工程师但愿基于一个强内存模型来编写代码。
  2. 编译器和处理器对内存的实现,但愿内存模型对他们的束缚越少越好,编译器和处理器但愿实现一个弱内存模型。

这两个因素是互相矛盾的,因此JSR-133专家组设计时须要考虑到一个好的平衡点:一方面为工程师提供足够强的内存可见性,另外一方面要对编译器和处理器的限制要尽可能松些。

咱们来举了例子:

int a=10;   //A
int b=20;   //B
int c=a*b;  //C

上面是一个简单的乘法运算,并存在3个happens-before关系:
1.  A happens-before B
2.  B happens-before C
3.  A happens-before C

这三个happens-before关系中,2和3是必须的,但1是没必要要的。所以,JMM把happens-before要求禁止的重排序分为两类:
1.会改变程序执行结果的重排序。
2.不会改变程序执行结果的重排序。


JMM对这两种不一样性质的重排序,采起了不一样的策略:
1.对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
2.对于不会改变程序执行结果的重排序,JMM要求编译器和处理器不作要求,能够容许这种重排序。
View Code

happens-before的定义与规则

JSR-133使用happens-before的概念来指定两个操做之间的执行顺序,因为这两个操做能够在一个线程内,也能够在不一样的线程之间。所以,JMM能够经过happens-before关系向工程师提供跨线程的内存可见性保证。

happens-before规则以下: 
1. 程序顺序规则:一个线程中的每一个操做,happens- before 于该线程中的任意后续操做。 
2. 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 
3. volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 
4. 传递性:若是A happens- before B,且B happens- before C,那么A happens- before C。

 

5.顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型为参考。

数据竞争与顺序一致性

当程序未正确同步时,就会存在数据竞争。数据竞争指的是:在一个线程中写一个变量,在另外一个线程读同一个变量,并且写和读没有经过同步来排序。 
当代码中包含数据竞争时,程序的执行每每产生违反直觉的结果。若是一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。 
JMM对正确同步的多线程程序的内存一致性作了以下保证: 
若是程序是正确同步的,程序的执行将具备顺序一致性(sequentially consistent),即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对经常使用同步原语(synchronized,volatile和final)的正确使用。

顺序一致性模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  1. 一个线程中的全部操做必须按照程序的顺序来执行。
  2. (无论程序是否同步)全部线程都只能看到一个单一的操做执行顺序。在顺序一致性内存模型中,每一个操做都必须原子执行且马上对全部线程可见。

顺序一致性内存模型为程序员提供的视图以下: 

在概念上,顺序一致性模型有一个单一的全局内存,这个内存经过一个左右摆动的开关能够链接到任意一个线程。同时,每个线程必须按程序的顺序来执行内存读/写操做。从上图咱们能够看出,在任意时间点最多只能有一个线程能够链接到内存。当多个线程并发执行时,图中的开关装置能把全部线程的全部内存读/写操做串行化。

顺序一致性内存模型中的每一个操做必须当即对任意线程可见,可是在JMM中就没有这个保证。未同步程序在JMM中不但总体的执行顺序是无序的,并且全部线程看到的操做执行顺序也可能不一致。好比,在当前线程把写过的数据缓存在本地内存中,且尚未刷新到主内存以前,这个写操做仅对当前线程可见;从其余线程的角度来观察,会认为这个写操做根本尚未被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存以后,这个写操做才能对其余线程可见。在这种状况下,当前线程和其它线程看到的操做执行顺序将不一致。

同步程序的顺序一致性

咱们接下来看看正确同步的程序如何具备顺序一致性。

class SynchronizedExample {
int a = 0;
boolean flag = false;

public synchronized void writer() {
    a = 1;
    flag = true;
}

public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
}
}
View Code

上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

在顺序一致性模型中,全部操做彻底按程序的顺序串行执行。而在JMM中,临界区内的代码能够重排序(但JMM不容许临界区内的代码“逸出”到临界区以外,那样会破坏监视器的语义)。JMM会在退出监视器和进入监视器这两个关键时间点作一些特别处理,使得线程在这两个时间点具备与顺序一致性模型相同的内存视图。虽然线程A在临界区内作了重排序,但因为监视器的互斥执行的特性,这里的线程B根本没法“观察”到线程A在临界区内的重排序。这种重排序既提升了执行效率,又没有改变程序的执行结果。 
从这里咱们能够看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽量的为编译器和处理器的优化打开方便之门。

未同步程序的顺序一致性

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。由于未同步程序在顺序一致性模型中执行时,总体上是无序的,其执行结果没法预知。保证未同步程序在两个模型中的执行结果一致毫无心义。 
和顺序一致性模型同样,未同步程序在JMM中的执行时,总体上也是无序的,其执行结果也没法预知。 
同时,未同步程序在这两个模型中的执行特性有下面几个差别:

  1. 顺序一致性模型保证单线程内的操做会按程序的顺序执行,而JMM不保证单线程内的操做会按程序的顺序执行(好比上面正确同步的多线程程序在临界区内的重排序)。
  2. 顺序一致性模型保证全部线程只能看到一致的操做执行顺序,而JMM不保证全部线程能看到一致的操做执行顺序。
  3. JMM不保证对64位的long型和double型变量的读/写操做具备原子性,而顺序一致性模型保证对全部的内存读/写操做都具备原子性。

对于第三个差别:在一些32位的处理器上,若是要求对64位数据的读/写操做具备原子性,会有比较大的开销。为了照顾这种处理器,java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的读/写具备原子性。当JVM在这种处理器上运行时,会把一个64位long/ double型变量的读/写操做拆分为两个32位的读/写操做来执行。这两个32位的读/写操做可能会被分配到不一样的总线事务中执行,此时对这个64位变量的读/写将不具备原子性。 
当单个内存操做不具备原子性,将可能会产生意想不到后果。请看下面示意图: 
这里写图片描述

如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操做被拆分为两个32位的写操做,且这两个32位的写操做被分配到不一样的写事务中执行。同时处理器B中64位的读操做被拆分为两个32位的读操做,且这两个32位的读操做被分配到同一个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。

相关文章
相关标签/搜索