JVM 的内存模型与线程

Java 内存模型(Java Memory Model, JMM)定义了多线程环境下共享变量的访问规则,是理解并发编程的基石。本文从硬件架构出发,逐步深入到 JMM 的核心机制与实践模式。

mindmap
  root((JMM))
    硬件基础
      CPU缓存层次
      缓存一致性协议
    JMM 抽象
      主内存 vs 工作内存
      八种内存操作
      happens-before 关系
    关键保证
      原子性
      可见性
      有序性
    实践工具
      volatile
      synchronized
      final

模式总览

# 模式名称 一句话口诀 适用场景
1 写刷读清 写入即刷盘,读取先清空 volatile / unlock 后的可见性
2 顺序锁 同把锁内,串行执行 synchronized 临界区保护
3 偏序传递 A先于B,B先于C,则A先于C happens-before 链式推理
4 不可变安全 构造完成前不逃逸,完成后不修改 final 字段的安全发布

一、从硬件到抽象:为什么需要 JMM

1.1 CPU 架构的性能鸿沟

现代 CPU 的处理速度远超主内存(DRAM)的响应能力。以 Intel Skylake 架构为例:

层级 典型延迟 容量范围
L1 Cache ~4 cycles 32KB
L2 Cache ~12 cycles 256KB
L3 Cache ~40 cycles 8-32MB
主内存 ~200+ cycles GB 级

这种数量级的差异迫使 CPU 引入多级缓存。但缓存带来了新的问题:多个处理器的缓存如何保持一致?

1.2 缓存一致性协议(MESI)

现代处理器使用 MESI 协议维护缓存一致性,其四个状态为:

状态 含义 说明
Modified 已修改 仅当前缓存拥有该数据,且与内存不一致
Exclusive 独占 仅当前缓存拥有该数据,与内存一致
Shared 共享 多个缓存拥有该数据,与内存一致
Invalid 失效 当前缓存该数据无效,需重新加载

状态转换示意:

1
2
3
4
5
6
7
8
9
初始:CPU0(E) ← 内存X=0

CPU0 写入 X=1:
E → M (独占→修改,通知其他 CPU 该地址为 I)

CPU1 读取 X:
CPU0 将 X 写回内存,状态变为 S
CPU1 从内存加载 X,状态为 S
结果:CPU0(S) ←→ 内存X=1 → CPU1(S)

1.3 为何需要 JMM

硬件层面的缓存一致性由处理器自动保证,但这对上层程序员是不透明的。编译器和运行时可能进行以下优化:

  • 编译器重排序:不改变单线程语义的前提下调整指令顺序
  • 处理器乱序执行:利用流水线并行执行无依赖的指令
  • 缓存可见性延迟:写操作对其他 CPU 立即可见并非保证

JMM 的作用是在这些底层优化之上,提供一个可预测的编程模型,明确告知程序员什么情况下可以安全地推断多线程间的执行顺序。


二、JMM 核心抽象

2.1 主内存与工作内存

JMM 定义了一套抽象的内存模型(JLS §17.4):

  • 主内存(Main Memory):所有变量存储的地方,对应物理内存
  • 工作内存(Working Memory):每个线程私有的缓存区域,对应 CPU 寄存器 + L1/L2/L3 缓存

线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存。

1
2
3
线程A工作内存 ← read/load → 主内存 → read/load → 线程B工作内存
↓ ↑
use → assign assign → use

2.2 八种内存操作(JLS §17.4.1)

JMM 定义了 8 种原子操作描述主内存与工作内存的交互:

操作 作用域 说明
lock 主内存 将变量标记为线程独占状态
unlock 主内存 释放变量的线程独占状态
read 主内存→工作内存 将变量值从主内存传输到工作内存
load 工作内存 read 的值放入工作内存副本
use 工作内存→执行引擎 将变量值传递给执行引擎
assign 执行引擎→工作内存 将执行结果赋值给工作内存变量
store 工作内存→主内存 将变量值从工作内存传输到主内存
write 主内存 store 的值写入主内存变量

基本执行约束(JLS §17.4.2):

  1. readload 必须按顺序执行,但可不连续(中间可插入其他指令)
  2. storewrite 必须按顺序执行,但可不连续
  3. 不允许丢弃最近的 assign,变量改变必须同步回主内存
  4. 新变量只能在主内存诞生,不可直接使用未初始化变量

典型的完整操作序列:

1
read → load → use → ... (业务计算) ... → assign → store → write

2.3 指令重排序

在不改变单线程执行结果的前提下,编译器和处理器可能对指令进行重排序。例如:

1
2
3
4
5
6
7
// 源代码
int a = 1; // A
int b = 2; // B
int c = a + b; // C

// 可能的重排序:A和B谁先执行不影响C的结果
// 处理器可能选择先执行B再执行A,以更好地填充流水线

JMM 对重排序的限制通过 happens-before 关系来表达。


三、happens-before 关系

3.1 什么是 happens-before

happens-before 是 JMM 定义的偏序关系(JLS §17.4.5)。若操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 在 B 之前执行。

注意:happens-before 不等于时间上的先后,而是指可见性保证。如果 A 不在 B 的 happens-before 链中,即使 A 实际先执行,B 也可能看不到 A 的结果。

3.2 happens-before 的八条规则

规则 内容 JLS 引用
程序次序规则 单线程内,书写在前面的操作 happens-before 后面的操作 §17.4.5
监视器锁定规则 unlock 操作 happens-before 后面对同一锁的 lock 操作 §17.4.5
volatile 规则 volatile 写操作 happens-before 后面对该变量的读操作 §17.4.5
线程启动规则 Thread.start() happens-before 线程内的每个动作 §17.4.5
线程终止规则 线程内所有操作 happens-before 线程终止检测 §17.4.5
线程中断规则 interrupt() 调用 happens-before 被中断线程检测到中断 §17.4.5
对象终结规则 构造函数结束 happens-before finalize() 开始 §17.4.5
传递性 A happens-before B 且 B happens-before C,则 A happens-before C §17.4.5

3.3 推导示例

示例 1:synchronized 保证可见性

1
2
3
4
5
6
7
8
synchronized (lock) {
x = 1; // 操作A:assign → store → write
} // 操作B:unlock

// 另一个线程
synchronized (lock) { // 操作C:lock
print(x); // 操作D:read x
}

推导链:

  • A happens-before B(程序次序规则)
  • B happens-before C(监视器锁定规则:unlock happens-before 后续 lock)
  • C happens-before D(程序次序规则)
  • 因此 A happens-before D(传递性),线程2 能看到 x=1

示例 2:volatile 的正确使用姿势

1
2
3
4
5
6
7
8
9
10
volatile boolean flag = false;
int value = 0;

// 线程A
value = 42; // A
flag = true; // B (volatile写)

// 线程B
while (!flag); // C (volatile读)
print(value); // D

推导链:

  • A happens-before B(程序次序规则)
  • B happens-before C(volatile 规则)
  • C happens-before D(程序次序规则)
  • 因此 A happens-before D,线程B 能看到 value=42

四、三大特性保障

4.1 原子性(Atomicity)

JMM 保证基本类型的读写操作是原子的(long/double 在某些平台可能非原子,但 JVM 实现通常保证)。

原子操作类型

  • read、load、use、assign、store、write(对 32 位及以下类型)
  • lock、unlock

非原子场景

1
2
3
4
5
6
7
8
9
// i++ 不是原子操作
// 字节码层面分解为:
// getfield i // read + load
// iconst_1
// iadd // use + assign(运算)
// putfield i // store + write

// 64位的 long/double 在非64位JVM上可能分两次读取
private volatile long counter; // 用 volatile 保证原子性

4.2 可见性(Visibility)

一个线程修改共享变量后,其他线程能够立即看到这个修改。

保证可见性的方式

方式 机制
volatile 每次写立即刷新到主内存,每次读从主内存刷新
synchronized unlock 时 flush 工作内存到主内存;lock 时清空工作内存并重新加载
final 正确构造的对象,其 final 字段对所有线程可见(无额外同步成本)

volatile 的实现细节

1
2
3
4
5
6
7
8
9
volatile int shared = 0;

void writer() {
shared = 1; // store + write 到主内存,并插入内存屏障
}

void reader() {
int local = shared; // read + load 从主内存,使本地缓存失效
}

volatile 写操作在 x86 平台上对应 lock addl $0x0 指令,起到两个作用:

  1. 将当前处理器的缓存行写回内存
  2. 使其他处理器的该缓存行无效

4.3 有序性(Ordering)

禁止特定类型的指令重排序,确保程序的执行顺序符合预期。

内存屏障(Memory Barrier)类型

屏障类型 示例指令 作用
LoadLoad Load1; LoadLoad; Load2 禁止 Load1 和 Load2 重排序
StoreStore Store1; StoreStore; Store2 禁止 Store1 和 Store2 重排序
LoadStore Load1; LoadStore; Store2 禁止 Load1 和 Store2 重排序
StoreLoad Store1; StoreLoad; Load2 禁止 Store1 和 Load2 重排序(开销最大)

volatile 的内存屏障插入策略(JSR-133 增强后):

  • volatile 写:前面插入 StoreStore,后面插入 StoreLoad
  • volatile 读:后面插入 LoadLoad 和 LoadStore

这确保了:

  • volatile 写之前的写操作不会被重排到后面
  • volatile 读之后的读/写操作不会被重排到前面

五、对象内存布局与指针压缩

5.1 对象的内存结构

HotSpot JVM 中,对象在内存中的布局如下:

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────┐
│ Mark Word │ ← 8 bytes(64位 JVM)
│ (对象头信息) │
├─────────────────┤
│ Class Pointer │ ← 4 bytes(压缩指针)或 8 bytes
│ (指向类元数据) │
├─────────────────┤
Instance Data│ ← 实例字段
├─────────────────┤
│ Padding │ ← 8 字节对齐填充
└─────────────────┘

5.2 Mark Word 的结构

Mark Word 是一个动态数据结构,根据对象状态复用存储空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
无锁状态:
┌──────────────────────┬─────────┬─────────┐
│ identity_hash │ age │ biased_lock_pattern (001) │
│ (25 bits) │(4 bits) │ (3 bits)
└──────────────────────┴─────────┴─────────┘

偏向锁状态:
┌──────────────────┬─────────┬──────────┬─────────┐
│ thread_id │ epoch │ age │ biased_lock_pattern (101) │
│ (54 bits) │(2 bits) │(4 bits) │ (3 bits)
└──────────────────┴─────────┴──────────┴─────────┘

轻量级锁状态:指向栈中锁记录的指针(62 bits)+ 标志位(00

重量级锁状态:指向互斥量(Monitor)的指针(62 bits)+ 标志位(10

5.3 指针压缩(Compressed OOPs)

64 位 JVM 默认开启指针压缩(-XX:+UseCompressedOops),将 64 位引用压缩为 32 位:

  • 堆内存 < 4GB:直接位移,无需解码
  • 4GB ≤ 堆内存 < 32GB:基址 + 偏移(默认策略)
  • ≥ 32GB:无法压缩,回退到 64 位指针

计算公式:

1
解码后地址 = 压缩值 << 3 + heap_base

启用压缩指针后,对象头的 Class Pointer 从 8 字节降为 4 字节,显著降低内存占用。


六、🔑 模式提炼

模式一:写刷读清

公式Assign → Store → Write (Flush) || Read → Load → Use (Refresh)

应用场景

场景 Flush 触发点 Refresh 触发点 效果
volatile 写 写后立即 store+write - 对所有后续读者可见
synchronized 解锁 unlock 前 flush lock 时 refresh 临界区变更对后续获取者可见
线程终止 线程结束 join() 返回 线程内所有操作对等待者可见

核心洞察:可见性问题的本质是"何时把工作内存的脏页刷到主内存",以及"何时废弃本地缓存重新加载"。happens-before 规则就是定义这两个动作的触发时机。

模式二:顺序锁

公式Lock(A) → { Critical Section } → Unlock(A) 串行化所有临界区

实现机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// synchronized 的字节码:monitorenter + monitorexit
public synchronized void increment();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
// 方法级别的 synchronized 在常量池添加 ACC_SYNCHRONIZED 标志
// JVM 进入/退出方法时自动获取/释放 monitor

// 代码块的 synchronized
synchronized (obj) { }
// 编译为:
// 0: aload_1
// 1: dup
// 2: astore_2 // 将 obj 存入局部变量
// 3: monitorenter // 获取锁
// ...
// n: aload_2
// n+1: monitorexit // 释放锁(正常路径)
// n+2: goto ...
// n+x: astore_3
// n+x+1: aload_2
// n+x+2: monitorexit // 释放锁(异常路径)

关键要点

  • 锁对象是 monitor 的载体,空对象不能作为锁
  • 异常退出时仍保证 monitorexit 执行(try-finally 语义)
  • 锁升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

模式三:偏序传递

公式A ≺ B ∧ B ≺ C ⇒ A ≺ C

推理链构建示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SafePublication {
final int x;
volatile int y;

SafePublication() {
x = 1; // A
y = 1; // B (volatile写,同时也是构造函数结束前的最后动作)
}

void reader() {
if (y == 1) { // C (volatile读)
assert x == 1; // D - 必然成立
}
}
}

推导:

  • A happens-before B(程序次序)
  • B happens-before C(volatile规则)
  • 构造函数结束 happens-before finalize(对象终结规则,这里隐含构造完成 happens-before 任何引用获取)
  • 因此 A happens-before C,进而 A happens-before D

实践价值:正确使用 volatile/final/synchronized 建立 happens-before 链,可以在无锁的情况下实现线程安全的可见性保障。


七、生产环境实践要点

7.1 volatile 的使用原则

适合场景

  • 状态标志位(如 boolean running = true
  • 单次写多次读的共享变量
  • 配合 synchronized 实现细粒度锁(如 StampedLock 的乐观读)

避免场景

  • 复合操作(i++、检查再执行)
  • 多个 volatile 变量间的依赖(不能保证整体原子性)

7.2 双重检查锁定的正确实现

Java 5 之前的 DCL 是错的,因为 volatile 不能保证有序性。JSR-133 修复后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private static volatile Singleton instance; // 必须 volatile

public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton(); // 创建分为三步:
// 1. 分配内存
// 2. 初始化对象
// 3. 将引用指向内存地址
// volatile 禁止 2-3 重排序,确保其他线程看到完整初始化
}
}
}
return instance;
}
}

不使用 volatile 的风险:线程A 执行到第3步(引用已赋值但未初始化),线程B 判断 instance != null 直接返回未初始化对象。

7.3 伪共享(False Sharing)问题

当两个独立变量位于同一缓存行(通常 64 字节)时,一个线程修改变量A会导致另一个线程的变量B缓存失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误示范
public class Counter {
private volatile long countA; // 可能和 countB 在同一缓存行
private volatile long countB;
}

// 正确做法:填充至缓存行大小
public class PaddedCounter {
private volatile long countA;
private long p1, p2, p3, p4, p5, p6, p7; // 56 bytes padding

private volatile long countB;
}

Java 8 引入 @Contended 注解自动处理:

1
2
3
4
5
6
7
8
public class Counter {
@Contended
private volatile long countA;

@Contended
private volatile long countB;
}
// 编译选项:-XX:-RestrictContended

7.4 监控与诊断

JVM 参数

参数 作用
-XX:+PrintAssembly 打印 JIT 生成的汇编代码(需 hsdis)
-XX:CompileCommand=print,*ClassName.methodName 指定方法打印汇编
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation 记录编译日志

jol(Java Object Layout)工具

1
2
3
4
5
6
7
8
9
10
11
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());

// 输出示例:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) e5 01 00 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

八、模式速查表

遇到的问题 应用的模式 具体方案 关键注意点
一个线程的修改对另一个不可见 写刷读清 volatile / synchronized volatile 只保证单次读写可见
复合操作结果错乱 顺序锁 synchronized / Lock 锁粒度尽可能小
多变量间可见性推理 偏序传递 建立 happens-before 链 确保链条完整,无断点
对象安全发布 不可变安全 final + 正确构造 this 引用不要在构造函数逸出
高并发下锁竞争激烈 锁优化 偏向锁 → 轻量级锁 → 分段锁 JDK 15+ 默认禁用偏向锁

参考文献

  1. Java Language Specification, Chapter 17.4 - JMM 官方规范
  2. JSR-133: Java Memory Model and Thread Specification - 内存模型修订文档
  3. 《Java 并发编程实战》Brian Goetz 等著
  4. Mechanical Sympathy Blog - False Sharing

3.JVM 的对象信息

  Java Object 除了基本的内存轮廓以外,还有:

  1. Mark Word(对象的 Hash Code 的缓存值、GC标志、GC年龄、同步锁等信息)。
  2. Klass Point(指向对象元数据信息的指针,指向 .class 的指针吗?不是,是指向方法区的类型元数据的指针。.Class文件实际上是那个区域的另一个入口了。)。
  3. padding。如果对象是8位对齐的(也就是最长标量类型对齐的),则不存在padding。

4.内存间(主内存与工作内存)相互操作

  Java内存模型(Java Memory Model)定义了八种内存操作(而不是字节码)。虚拟机在是现实必须保证每一种操作都是原子的、不可再分的(对于 double 和 long 类型的变量来说,load、store、read 和 write 操作在某些平台上可以例外):

  1. lock 把主内存变量为一个线程锁定起来。
  2. unlock 把主内存的变量解锁,这样其他线程才能锁定。
  3. read 把一个变量的值,从主内存读到工作内存里。是 load 指令的前置动作。
  4. load 把read出来的变量,放到工作内存的副本里。
  5. use 把工作内存的值传给工作执行引擎。
  6. assign 把执行引擎里得到的值传给工作内存的变量副本。它是一种工作内存的局部写。
  7. store 把工作内存中的变量的值传递给主内存。

  实际上的执行顺序恐怕是 read->load->use->assign->store-> write。

  如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行 read 和load 操作,如果要把变量从工作内存同不会主内存,就要执行 store 和 write 操作。 JMM 只要求上述两类操作必须按顺序执行,没有保证必须是连续执行,也就是说在 read 和 load之间、store 和 write 之间是可插入其他指令的。如对主内存的变量 a、b 进行访问的时候,可能出现 read a、read b、load b、load a 的操作顺序。

  除此之外, JVM 还规定了额外的指令执行的偏序规则(正好也有八条):

  • 不允许 read 和 load、store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起了写回但工作内存不接受的情况。
  • 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中发生了改变必须(最终)把该变化同步回主内存里去。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load 或者 assign)的变量,换句话说就是对一个变量实施 use 和 store操作之前,必须经过 assign 和 load 的操作。
  • 一个变量在同一个时刻只允许一条线程对它进行 lock 操作,且 lock 操作可以被同一个线程执行多次(多种可重入锁的底层机制就在这里了)。而且只有执行相同数量的 unlock 操作,才能彻底解锁该变量。
  • 如果对一个变量进行 lock 操作,会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 和 assign 操作。也就是说,这是一个 flush 加上 reload的过程。
  • 如果一个变量没有被 lock 锁住,则 unlock 非法,只有本线程才能unlock。
  • 对一个变量进行unlock操作之前,必须先把变量同步回主内存中(执行 store 和 write 操作)。也就是说,变量被线程锁住以后,不是在主内存上工作,而是在自己的工作内存里被使用的,这也印证了上面的八种指令中的 use 必须在 load 之后工作,执行引擎必须使用 use 的印象

5.volatile关键字

  volatile 关键字具有可见性,会使得每次写操作,都会导致全flush 的出现(assign必然导致 store 和 write 回主内存),读操作必须read + load至工作内存, use 到执行引擎(而不能只是use上次留在工作内存里的值),必然总是得到最新的值,不管中间是否有不一致的暂时情况发生,读的语义必然是一致正确的。而如果没有这条语义,use得到的值,可能是之前 use 和 assign 得到的值。

  注意,如果使用字节码分析多线程操作,即使只出现一条指令,也不能认为实际执行的机器指令是原子化的,**但如果出现多条字节码指令,那么必然操作没有原子性。**这也是 volatile 修饰的变量只是轻量级同步,不能做到真正互斥原子化的原因。它只保证了可见性。

  因此,只有两种情况,不必然要使用标准同步机制:

  1. 远算结果不依赖指定非栈上变量的当前值,或者能够确保单线程修改指定变量的当前值。
  2. 变量不需要与其他变量参与同一个不变性约束。

  此外,volatile关键字还可以通过插入内存屏障(memory barier)阻止内存指令重排(instruction reorder),阻止特定的赋值顺序被打乱。这点在 Java 5以前是做不到的,也就会经常性导致 Double Check Lock 在 Java 5以前失败。具体地说,相关联的操作是不可重排序的。相关联的read->load->use/assign->store->write可以看做是不可被重排插入中间指令的,一个指令 read 先于另一个指令 read,那么所有相关联的指令都是前者先于后者。这被称为“线程内表现为穿行语义”(Within-Thread As-If-Serial Semantics)。

6.Java内存模型的(Java)的特性

6.1 原子性(Atomicity)##

  8个操作,read、load、use、assign、store、write这六个操作是必须原子的(64字节的 long、double 非原子性是可以由lock 和 unlock 的更强原子语义包裹起来规避掉的)。lock 和 unlock 操作虽然不是字节码,但几乎同意的 monitoerenter和monitorexit却是字节码指令。

6.2 可见性(Visibility)##

  一个线程的修改,立刻可以被另一个线程看到,方法主要有三个:

  • 同步块
  • final (final 并不是不可更改的,所以依然有工作内存修改后flush的问题)
  • volatile

6.3 有序性(Ordering)##

  volatile和同步块可以保证这点。方法内的指令不会被重排,是一个特别重要的不会产生特别副作用的保证。

6.4 volatile 和同步块比较##

  volatile不具有原子性,其他场景volatile和同步块都可以使用。

6.5 先行发生原则(happens-before)##

  JVM 为程序中所有的操作定义了一个偏序关系(偏序关系 π 是集合上的一种关系,据有反对称、自反和传递属性。但对于任意两个元素x,y来说,并不需要一定满足 x π y, y π x的关系。我们每天都在使用偏序关系表达喜好。),称之为 Happens-Before。只有操作 A 和操作 B 之间满足 Happens-Before 关系,才能保证
保证操作 B 一定能够看到操作 A 的结果。

  Happens-Before 的八条原则包括:

  • 程序顺序原则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作线性发生于书写在后面的操作。这一条并不绝对,首先要考虑控制流循环跳转的问题,其次是,如果后操作无法感知前操作(即不存在依赖关系),则指令重排仍然可能发生。
  • 监视器锁定原则(Monitor Lock Rule):一个 unlock 操作时间顺序上先行发生于后面对同一个锁的 lock 操作。(单纯的lock 操作语义只提供了可见性,这条原则还保证了有序性。)
  • volatile 变量原则(volatile variable rule):对 volatile 变量的写入操作,必须要在读取操作时间顺序之前进行。
  • 线程启动规则(Thread Start Rule):Thread对象的 start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中所有操作,都先行发生于线程的终止检测。常见终止检测是 Thread.join() 的返回,Thread.isAlive()的返回。
  • 线程中断原则(Thread Interruption):对线程 interrupt() 方法的调用先行发生于被中断线程检测中断事件的发生。常见检测事件的方法是 Thread.interrupted()。
  • 对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
  • 传递性(Transitivity) 操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,操作 A 先行发生于操作 C。