Java使用同步/易失性时可视化读/写障碍

示例

众所周知,我们应该使用synchronized关键字来使方法或块的执行互斥。但是我们当中很少有人可能不会意识到使用synchronized和volatile关键字的另一个重要方面:除了使代码单元原子化之外,它还提供了读/写障碍。这是什么读/写障碍?让我们用一个例子讨论一下:

class Counter {

  private Integer count = 10;

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

  public Integer getCount() {
    return count;
  }
}

假设一个线程AincrementCount()首先调用,然后另一个线程B调用getCount()。在这种情况下,不能保证B会看到的更新值count。它可能仍会显示count为10,甚至也有可能永远不会看到更新的值count。

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

但是在我们的例子中,由于线程B尚未使用对的同步访问count,它可能是引用count存储在寄存器中的值,并且可能永远不会看到线程A的更新。要确保B看到最新的count值,我们还需要使其getCount()同步。

public synchronized Integer getCount() {
  return count;
}

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

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