Java内存模型解决可见性和有序性问题

可见性有序性问题,是由于CPU缓存和编译优化导致的,
按需禁用缓存和编译优化,既解决了并发问题,又保证了基本的性能优化。

使用volatile禁用CPU缓存

使用 volatile 描述的变量,编译器会直接操作内存,而不是从缓存中存取值。

Happens-Before 规则 增强了 volatile 修饰的变量


class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}

假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 v=true 写入内存;
假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v
如果线程 B 看到 v == true 时,那么线程 B 看到的变量 x 是多少呢?

在1.5以下版本中,可能是 42 也可能是 0x 并没有被 volatile 修饰,可能会从缓存中取值)。
在1.5及以上版本中,由于引入了 Happens-Before 原则 x 的值则为 42

Happens-Before 原则是指:前面一个操作的结果对后续操作是可见的。Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

顺序性规则

按照代码的顺序,前面的操作相对于后面的操作是可见的。

volatile 变量规则

一个 volatile 变量的写操作, 相对于后续对这个 volatile 变量的读操作是可见的。

传递性规则

如果 A 相对于 B 是可见的,且 B 相对于 C 是可见的,那么 A 相对于 C 也是可见的。

示例代码中的传递性规则

由此可见:

  1. 根据顺序性规则x=42 相对于 写变量 v=true 是可见的;
  2. 根据volatile变量规则:写变量 v=true 相对于 读变量 v=true 是可见的。
  3. 根据传递性规则x=42 相对于 读变量 v=true 是可见的。

如果线程 B 读到了 v=true,那么线程 A 设置的 x=42 对线程 B 是可见的。

管程中锁的规则

一个线程对一个锁临界区执行完解锁后,变量的值对于其他线程后续对这个临界区做加锁操作是可见的。

synchronized 就是Java中管程的实现。


synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁

假设 x 的初始值是 10;
线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁);
线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12

线程 start() 规则

主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

线程 join() 规则

如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 相对于该 join() 操作的返回 是可见的。

Thread B = new Thread(()->{
  // 此处对共享变量var修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66