该专栏源文件与 java-concurrency 同步,源码与 github-java-concurrency 同步。在线阅读

基础概念

  • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
  • 有序性:即程序执行的顺序按照代码的先后顺序执行

内存模型结构

线程之间如何通信?

命令式编程中线程通信的方式:

  • 共享内存
  • 消息传递

在共享内存模型里线程之间共享内存的公共状态,在消息传递模型里,线程之间靠消息的发送接收来显示的进行通信。

Java使用共享内存模型进行线程通信。

Java共享内存模型结构(JMM)

Java堆和方法区是多个线程共享的数据区域。多个线程可以操作堆和方法区中的同一个数据。局部变量,方法定义参数和异常处理参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型影响。
Java内存模型的英文名称为Java Memory Model(JMM),其并不像JVM内存结构一样真实存在,而是一个抽象的概念。
Java内存模型-基础概念

分析上图内容可知:线程A/B之间通信主要经历步骤为

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

从整体上看,这个通信过程需要经过主内存。JMM通过控制主内存与每个线程本地内存之间的交互来提供内存可见性保证。

重排序

为什么要重排序?

为了提高性能,编译器与处理器通常会对指令做重排序,通常为3种

  1. 编译器优化的重排序,不改变单线程语义的情况下重新安排语句执行顺序。
  2. 指令级并行的重排序,现在处理器采用指令并行技术,可将多条指令并行执行。如果不存在数据依赖性,可以改变语句对应指令顺序。
  3. 内存系统的重排序,由于处理器使用缓存和读写缓存区,使得加载和存储操作看上去是乱序执行。

源码如何变成执行指令?

步骤:源代码->1编译器优化重排序->2指令级并行重排序->3内存系统重排序->最终执行指令
对于步骤1是编译器重排序,步骤2、3是处理器重排序。

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

内存屏障

Load 表示读,Store 表示写。内存屏障的四种类型如下

LoadLoad屏障

抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障

抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

LoadStore屏障

抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障

抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

happens-before 语义

从JDK5开始,Java使用新的JSR-133内存模型进行管理。JSR-133使用happens-before概念来阐述操作之间的可见性。

在JMM中如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间必须要存在happens-before关系(2个操作可以是同一线程或不同线程中)。

JMM把happens-before重排序分为2类:

  1. 会改变程序结果的重排序,JMM要求编译器和处理器禁止这种重排序。
  2. 不会改变程序结果的重排序,JMM允许这种重排序。

分析可知JMM遵循一个基本原则:只要不改变程序执行结果(单线程和同步的多线程)编译器和处理器怎么优化都可以,比如:

  • 一个锁只被单线程访问,那么锁可以消除
  • 一个volatile变量只被单线程访问,编译器可以把它当做普通变量使用

happens-before 规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before于线程中的任意后续操作
  • 监视器锁规则:一个锁的解锁,happens-before于随后对这个锁的加锁
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
  • 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C
  • start()规则:如果线程A执行操作ThreadB.start()(启动B线程),那么A线程的ThreadB.start()操作happens-before于B线程中的任何操作
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么B线程的任意操作happens-before于A从ThreadB.join()成功返回

as-if-serial 语义

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

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

相关文章: