【Java】 thread 内存模型

Java 线程内存模型, 是由虚拟机规范定义, 旨在屏蔽不同平台硬件和操作系统的差异,
使 Java 程序在不同平台表现出相同的并发行为和结果, 进行无歧义的内存并发访问操作,
同时让虚拟机也能利用不同硬件和平台的特性去表现更好的性能.
(与此相关的规范是: JSR-133)

主内存和工作内存

首先要先介绍主内存和工作内存, 这两者到底是什么呢?
周志明的书<深入理解Java虚拟机>中有一幅图, 描述两者的关系:

如果要快速地理解他们之间的关系, 可以类比 CPU/高速缓存/内存的关系(对应线程/工作内存/主内存).

映射到底层来看, 主内存在物理硬件内存中, 工作内存可能会存在于寄存器或高速缓存.

线程操作术语和框架

以下操作都是原子的:

  • lock: 锁定一个变量为某个线程独占

  • unlock: 释放已lock的变量

  • read: 从主内存读取 variable 到工作内存

  • load: 把 read 到的 variable 放入工作内存的变量副本

  • use: 把工作内存的变量传递给执行引擎

  • assign: 执行引擎通过此操作值赋给工作内存的变量

  • store: 把工作内存的一个变量值送到主内存中

  • write: 把 store 送到主内存的值写到主内存变量中

以上八个操作更详细的介绍可以从这个JVM Specs 文档中查阅.

规则

有了以上8种基本操作, 可以观察到, 这些操作的一些 pattern 组合是有意义的,
一些是不合法无意义的. 因此我们还需要一些规则来限定这些操作的顺序和组合, 如下:

变量规则

  • 不允许丢失最近的assign, 必须把工作内存的变化同步回主内存
  • 不允许线程没有发生assign就把内容同步回主内存.
  • 新线程创建时工作内存是空的, 而且需从主内存获取(即use/store前必须已经assign/load)
  • read+load和store+write是成对按顺序出现的, 从主内存read了, 工作内存就必须接受; store了主内存, 主内存就必须write

lock 规则

  • 同一时刻只能有一个线程进行lock操作, 但同一线程可以lock多次, 同时unlock也需要执行相同的次数.
  • 不允许 lock 未被 lock 的以及被其他线程 lock 住的变量

lock和变量互动规则

  • unlock 时, 必须把内容同步会主内存
  • 当 lock 某个变量时, 会清空工作内存中的该变量的值, 当执行引擎使用到该变量时, 从主内存进行 load 或 assign.

先行发生原则

观察上面这些规则的设计, 可以发现是比较严谨繁琐的. 但写代码时, 我们判断是否线程安全的方法用的是先行发生(happens-before)原则. 如果程序不在该规则及其推导中, 那么就不是线程安全的. 以下是主要规则:

  • 一个线程内, 代码按控制流顺序执行
  • 监视器上的 unlock 先于器 lock 操作
  • volatile 的 write 先于随后的 read
  • 该线程的 start() 的调用先于该线程的所有 action
  • 该线程的所有 action 先于该线程的 join() 返回
  • 对象初始化操作先于其他 action

参考


JSR-133
Java Memory Model Under The Hood
VM Spec Threads and Locks
Chapter 17. Threads and Locks