使用 synchronized volatile 时可视化读写障碍
我们知道我们应该使用 synchronized
关键字来执行方法或块独占。但是我们很少有人可能没有意识到使用 synchronized
和 volatile
关键字的另一个重要方面: 除了使代码单元成为原子之外,它还提供了读/写屏障。这个读/写障碍是什么?让我们用一个例子讨论这个:
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
的值并查看所有更新。
同样的可见性效果也适用于 volatile
读/写。在写入 volatile
之前更新的所有变量将被刷新到主存储器,并且在 volatile
变量读取之后的所有读取都将来自主存储器。