使用 synchronized volatile 时可视化读写障碍

我们知道我们应该使用 synchronized 关键字来执行方法或块独占。但是我们很少有人可能没有意识到使用 synchronizedvolatile 关键字的另一个重要方面: 除了使代码单元成为原子之外,它还提供了读/写屏障。这个读/写障碍是什么?让我们用一个例子讨论这个:

class Counter {

  private Integer count = 10;

  public synchronized void incrementCount() {
    count++;
  }

  public Integer getCount() {
    return count;
  }
}

让我们假设一个线程 A 先调用 incrementCount() 然后另一个线程 B 调用 getCount()。在这种情况下,无法保证 B 将看到 count 的更新值。它仍然可以看到 count 作为 10,即使它也可能永远不会看到 count 的更新价值。

要理解这种行为,我们需要了解 Java 内存模型如何与硬件架构集成。在 Java 中,每个线程都有自己的线程堆栈。该堆栈包含:方法调用堆栈和在该线程中创建的局部变量。在多核系统中,两个线程很可能在不同的内核中并发运行。在这种情况下,线程堆栈的一部分可能位于核心的寄存器/缓存中。如果在一个线程内,使用 synchronized(或 volatile)关键字访问一个对象,则在 synchronized 块之后该线程将该变量的本地副本与主内存同步。这会创建一个读/写屏障,并确保线程正在查看该对象的最新值。

但在我们的例子中,由于线程 B 没有使用对 count 的同步访问,它可能是指存储在寄存器中的 count 的值,并且可能永远不会看到来自线程 A 的更新。为了确保 B 看到计数的最新值,我们需要使用 getCount() 同步也是如此。

public synchronized Integer getCount() {
  return count;
}

现在,当线程 A 完成更新 count 时,它会解锁 Counter 实例,同时创建写屏障并将该块内完成的所有更改刷新到主存储器。类似地,当线程 B 获取对 Counter 的同一实例的锁定时,它进入读屏障并从主存储器读取 count 的值并查看所有更新。

StackOverflow 文档

同样的可见性效果也适用于 volatile 读/写。在写入 volatile 之前更新的所有变量将被刷新到主存储器,并且在 volatile 变量读取之后的所有读取都将来自主存储器。