《Java并发编程的艺术》第三章内存模型(基本概念)

并发的两个关键问题

  • 通信:线程之间传递信息
  • 同步:控制不同线程之间操作发生相对顺序的机制(前因不得先于后果发生)

Java采用共享内存模型,通信通过对一个堆上共享变量的读写来进行隐式通信,必须显式的指定某个方法或者某段代码需要线程之间互斥执行。

JMM抽象结构

在JMM中每个线程中存在一个本地内存的抽象概念(不存在,涵盖了缓存,写缓冲区等),对共享变量的读写会缓存到本地内存,这时其他线程是不可见的,只有刷新到主内存时其他线程才可以看到。
JMM决定一个线程对共享变量的写入何时对其他线程可见(刷新到主内存),它定义了线程和主内存的抽象关系。
《Java并发编程的艺术》第三章内存模型(基本概念)

指令重排序

同步问题那,为什么线程之间的相对顺序可能不一致,一部分原因就是指令重排序(还有一部分就是线程执行的时间了)
为了提升性能,编译器和处理器会对指令进行重排序。Java源码可能会经过以下三种重排序。
《Java并发编程的艺术》第三章内存模型(基本概念)

处理器类型 Load-Load Load-Store Store-Store Store-Load 数据依赖
x86 N N N Y N

由于写缓冲区的存在,所以是允许写-读这种重排序的
这里的数据依赖性是指,两次操作对同一个变量进行,且至少有一个操作为写操作
为了隔绝其他的重排序,Java编译器会在指令序列的适当位置插入内存屏障指令。

屏障类型 指令实例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2以及后续所有转载指令的装载。
StoreStore Store1;StoreStore;Store2 确保Store1的数据先于Store2写之前刷新到主内存
LoadStore Load1;LoadStore;Store2 确保先读取后写入并刷新到主存
StoreLoad Store1;StoreLoad;Load2 确保先写入主存再读,是一个全能型的屏障具备上述全部功能,开销也比较昂贵,会将写缓冲区的数据全部刷新到主内存中

happens-before

从JDK5开始,Java使用新的JSR-133内存模型基于happens-befor的概念来阐述两个操作是否在一个线程内。
注意happens-before关系并不是指前一个操作要在后一个操作之前执行,只要前一个操作的对后一个操作有作用的结果先于后一个操作该结果的调用使用前产生即可。
我感觉就是一种因果论的关系,原因得产生于结果之前。

  • 程序顺序规则:同线程中的每个操作happns-before于该线程的后续操作
  • 监视器锁规则:对一个锁的解锁先于其他后续对这个锁的加锁
  • volatile规则:对一个volatile域的写先于任意后续对它的读
  • 传递性:a先于b,b先于c,那么a先于c。

JMM是基于这些规则来决定哪些重排序不能进行的。

as-if-serial语义和程序顺序规则

刚才提到happens-before只看结果是否与想要的结果一致就行不一定操作顺序得与。就是as-if-serial,无论如何重排序都与程序顺序执行的结果是一致的。
举个例子: 要求a+b =c .步骤分为1.对a赋值,对b赋值,对a、b求和。a和b的赋值是没有数据依赖性的关系的,所有先对a还是先对b是没有影响的。但是求和就不能先于a,b赋值
这里的程序顺序规则仿佛就是多线程版的as-if-serial,只要两个线程中的操作的结果不会影响到其他线程,那也是可以重排序的。
举个例子: 还是刚才那个例子一个线程对a赋值,一个线程对b赋值,一个线程求和,如果需要与预期一致,就得保证a,b线程产生的结果先于最后个线程。

重排序对多线程的影响

如果没有数据依赖性,默认是可以重排序的。
实例域:
a = 0;
flag = false;
A线程:
a = 1;
flag = true;
B线程:
if(flag) b=a*2;

《Java并发编程的艺术》第三章内存模型(基本概念)
还可能:
《Java并发编程的艺术》第三章内存模型(基本概念)
这个例子说明,单线程的重排序没啥影响,多线程中就考虑顺序一致性模型了。

顺序一致性内存模型

主要有两大特性:

  1. 一个线程所有操作必须按照程序的顺序执行
  2. 所有线程只能看到一个单一的操作顺序(无论同步与否)。
    就有点类似于将每个多线程串行化的感觉。
    《Java并发编程的艺术》第三章内存模型(基本概念)
    如果同步:
    《Java并发编程的艺术》第三章内存模型(基本概念)
    如果不同步:
    《Java并发编程的艺术》第三章内存模型(基本概念)
    内部顺序一致且两个线程看到的执行都是如此
    不过在JMM中并不保证内部不会重排序,也就是如果不保证同步,不仅线程之间会是乱序,线程内也是乱序。
    也就是说如果同步的情况下,这样也是允许的:
    《Java并发编程的艺术》第三章内存模型(基本概念)
    而对于未同步或者未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值要么是之前某个线程写入的值,要么是默认值。
    除了上述所诉与顺序一致性模型不一样的地方以外(1.线程内顺序执行,2.所有线程看到的执行顺序一致(想想“本地内存”)) 还有一点就是64位的long和double写操作不提供原子性。

(提示:重排序是在即时编译阶段才可能产生的优化,或者1.3版本以前的静态编译(俺试了好久,就是没看到指令重排序,搜了一下才知道的))