Java 多线程开发中常遇到的问题就是如何在线程间共享变量,其中一个重要的原则就是 happens-before 。

可见性

如果线程 A 中的操作对线程 B 可见,那么 A 操作结果则对 B 可见

现代操作系统都是多核 CPU,CPU 内部的多级缓存,内存,磁盘组成计算机的所有存储单元。下图是一个简单的示例:
操作系统内存模型

Java 的线程在某个CPU内部执行,此时对某个变量的操作如果要保证能够对其他线程可见,则需要通过关键字 volatile 进行修饰。 volatile 关键字修饰的变量默认具有 happens-before 语义。即:

Happens-before 关系

两个操作可以具有 happens-before 关系,如果一个操作 happens-before 另外一个,那么第一个操作对第二个是可见的,并且第一个一定会先于第二个发生。

假设现有两个操作 X 和 Y ,使用 hb(X, Y) 代表 X happens-before Y。

  • 如果 X 和 Y 在同一线程内,并且程序中 X 出现在 Y 之前,则 hb(X, Y)
  • 一个对象的构造 Happens-before 该对象的回收
  • 如果 X 和 Y 操作被 synchronized 包围,则 hb(X, Y)
  • 如果 hb(X, Y) 并且 hb(Y, Z) 则 hb(X, Z)

需要注意的是如果两个操作存在 happens-before 关系,并不一定要求这两个操作在实际代码中保持该顺序。如果重排序后不影响执行结果,那么这也是可接受的。例如,一个线程内部会创建并初始化一个对象,那么这个对象的创建可以发生在线程开始之前,初始化可以发生在线程开始之后。另外一点就是具有 happens-before 关系的操作与不受该关系约束的操作互不干扰。

Happens-before 关系往往会带来数据竞争(如果存在针对同一个变量有两个或以上的操作,这些操作不存在 happens-before 关系约束,这种情况称为数据竞争),下面这些同步操作是存在 happens-before 关系的:

  • 一个对象 monitor 的 unlock 操作 happens-before 该 monitor 的 lock 操作
  • volatile 变量的写操作 happens-before 后续的所有读写操作
  • 线程 start 方法的调用 happens-before 该线程的启动
  • 一个线程所有操作 happens-before 所有其他 join 的线程成功返回结果
  • 任何对象的初始化 happens-before 该对象的其他操作

参考:Java doc 地址