重读Java并发编程艺术(2) - Java内存模型(JMM)

1. Java内存模型的基础

1.1 并发编程模型的两个关键问题

  • 线程间如何通信(命令式编程的两种通信机制:共享内存消息传递
  • 线程间如何同步(同步:控制不同线程间操作发生的相对顺序的机制)

1.2 Java内存模型抽象结构

Java线程之间的通信由Java内存模型(JMM)控制。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本
(本地内存是JMM的一个抽象概念,并不真实存在。涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化)
在这里插入图片描述
如上图,如果线程A要和线程B通信,将经历如下2个步骤:

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

JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

2. 重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

2.1 分类

  • 编译器优化的重排序
  • 指令级并行的重排序(处理器)
  • 内存系统的重排序(处理器)

2.2 as-If-serial语义

意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。
编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

2.3 内存屏障

JMM通过限制编译器和处理器的重排序来保证内存可见性。
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM 把内存屏障指令分为4类:

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

3. 顺序一致性

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

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

4. happen-before

4.1 定义(JSR-133)

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么这种重排序并不非法(即 JMM 允许这种重排序)。

4.2 和 as-If-serial 联系

本质上是一样的:

  • as-if-serial 语义保证单线程内程序的执行结果不被改变,
  • happens-before 关系保证正确同步的多线程程序的执行结果不被改变。

4.3 happens-before 规则(JSR-133)

  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. start() 规则:如果线程 A 执行操作 ThreadB.start()(启动线程B),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
  6. join() 规则:如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。

5. volatile 的内存语义

5.1 volatile 的特性

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

5.2 volatile 内存语义

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

从内存语义的角度来说,volatile的写-读 与 锁的释放-获取 有相同的内存效果。

5.3 volatile 内存语义的实现

为了实现 volatile 内存语义, JMM 会分别限制编译器重排序和处理器重排序。

5.3.1 JMM 针对编译器指定的 volatile 重排序规则:
是否能重排序第二个操作
第一个操作普通读/写volatile读volatile写
普通读/写--NO
volatile 读NONONO
volatile 写-NONO

上表可总结规律:

  • 第二个操作为 volatile 写时,不管第一个操作是什么,都不能重排序。
  • 第一个操作时 volatile 读时,不管第二个操作时什么,都不能重排序。
  • 第一个操作时 volatile 写,第二个操作是 volatile 读时,不能重排序。
5.3.2 JMM 内存屏障插入策略(保守策略)

为了实现 volatile 的内存语义,编译器在生成字节码时在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

实际执行时,编译器可以根据具体情况省略不必要的屏障。

6. 锁的内存语义

6.1 内存语义

  • 锁的释放(解锁): 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  • 锁的获取(加锁):当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

6.2 锁内存语义的实现

两种方式:

  • 利用 volatile 变量的写-读所具有的内存语义。(如 ReentrantLock 的公平锁)
  • 利用 CAS 所附带的 volatile 读和 volatile 写的内存语义。 (如 ReentrantLock 的非公平锁)

7. final 域的内存语义

7.1 final 域的重排序规则

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

7.2 final 内存语义的实现

7.2.1 写 final 域的重排序规则
  • JMM 禁止编译器把 final 域的写重排序到构造函数之外。
  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。
7.2.2 读 final 域的重排序规则
  • JMM 禁止处理器重排序一个线程中初次读对象引用初次读该对象包含的 final 域这两个操作。
  • 编译器会在读 final 域操作之前插入一个 LoadLoad 屏障。

8. 双重检查锁定与延迟初始化

8.1 基于 volatile 的解决方案(JDK5以上)

示例代码(注意 instance 对象声明为 volatile 型):

public class SafeDoubleCheckedLocking {
	//注意 instance 对象声明为 volatile 型
	private volatile static Instance instance;
	
	public static Instance getInstance(){
		if(instance == null){
			synchronized(SafeDoubleCheckedLocking.class){
				if(instance == null){
					instance = new Instance();
				}
			}
		}
		return instatnce;
	}
} 

8.2 基于 类初始化的解决方案

JVM在执行类的初始化期间,会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

public class InstanceFactory{
	private static class InstanceHolder{
		public static Instance instance = new Instance();
	}
	public static Instance getInstance(){
		//这里将导致 InstanceHolder 类被初始化
		return InstanceHolder.instance;
	}
}

8.3 方案对比

  • 如果确实需要对实例字段使用线程安全的延迟初始化,使用基于 volatile 的延迟初始化方案;
  • 如果确实需要对静态字段使用线程安全的延迟初始化,使用基于类初始化的方案。

9. 各种内存模型之间的关系

从强到弱:

  • 顺序一致性内存模型:理论参考模型
  • JMM:语言级的内存模型
  • 处理器内存模型:硬件级的内存模型

(越是追求执行性能的语言/处理器,内存模型设计得越弱)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值