Java内存模型

主内存与工作内存

  Java内存模型规定了所有变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。三者的关系如下图。

image.png

内存间交互操作

  主内存和工作内存之间有具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java 内存模型定义了以下8种操作来完成,每一种操作都是原子的、不可再分的

  • lock(锁定):作用于主内存的变量,它把一个变量标识位一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):用户工作内存的变量,它把read操作从主内存得到变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

volatile 型变量

  关键字 volatile 可以说是Java虚拟机提供的最轻量级的同步机制。当一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,可见性是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。volatile变量在各个线程的工作内存中不存在一致性问题。

  因为volatile变量只能保证可见性,不符合以下两条规则的运算场景,仍要加锁来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值。
  • 变量不需要与其它的状态变量共同参与不变约束。

  第二种特性就是禁止指令重排序优化。重排序在单线程内没有问题,因为一个线程的方法执行过程中是串行的。但在多线程中会出现问题,如下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean initialized = false;

// 以下代码在 A 线程中执行
// 读取配置信息,用于初始化,完成后,将initialized设置为true,以通知其它线程初始化完成
initConfig();
initialized = true;

// 以下代码在 B 线程中执行
// 等待线程 A 初始化完成
while(!initialized) {
sleep();
}
// 使用线程 A 中初始化好的信息
doSomething();

如果initialized 不是 volatile修饰的,那可能会由于指令重排序的优化,导致 initialized = true; 被提前执行,从而导致 B线程调用 doSomething(); 方法时出现错误。

  volatile变量的原理是什么呢?所有对 volatile 变量进行写操作后,其之后都会立即追加一行汇编代码:lock addl $ 0x0, (%esp) 。这句指令的意思是把 ESP 寄存器的值加上0,显然是一个空操作,关键在于 lock 前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,所以通过一个这样空操作,可以让前面 volatile 变量的修改对其它 CPU 立刻可见。由于这句指令把修改同步到了内存,意味着之前所有的操作都已经完成,这就相当于一个内存屏障,指令重排序时不能把屏障后面的指令重排序到内存屏障之前的位置,屏障后面的指令再怎么重排也无法越过屏障。

  同时,在读 volatile 变量前,也会加入一个读屏障,会从主内存,保证了 initConfig() 一定会在 initialized = true;之前执行。

原子性、可见性、有序性

原子性

  由Java内存模型来直接保证原子性变量操作包括 read、load、assign、use、store 和 write,可以大致认为基本类型的访问和读写是具备原子性的(long 和 double 除外,无需在意)。
  如果需要一个更大范围的原子性保证,Java内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把这两个指令直接开放给用户,但提供了更高层次的字节码指令 monitorentermonitorexit 来隐式地使用这两个操作,这两个字节码反映到 Java代码中就是同步块 synchroized 关键字。

可见性

  可见性是指当一个新出修改了共享变量的值,其他线程能能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。无论是普通变量还是volatile变量都是有可见性的,只是volatile的规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。

  除了 volatile, synchroized 和 final 也能保证可见性。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把次变量同步回主内存中”这条规则获得的。 final 的可见性是指:被final修饰的字段在构造器中一旦初始化完成,且构造器没有把 this 的引用传递出去,那在其它线程中就能看见 final 字段的值。

有序性

  Java中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

  Java提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性。volatile关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由“一个变量在同一时刻只允许一条线程对其它人进行操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。