同步简单的 getter 有什么用?

What could be the use of synchronizing the simple getter?

在 Goetz 和 Co 的著名著作 "Java concurrency in practice" 中,在其中一个 "good" 示例中,我发现了以下内容:

Listing 2.8
@ThreadSafe
public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;
    public synchronized long getHits() { return hits; } //  <-- here is the problem!
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
            synchronized (this) {
                ++hits;
                if (i.equals(lastNumber)) {
                    ++cacheHits;
                    factors = lastFactors.clone();
                }
            }
            if (factors == null) {
            factors = factor(i);
            synchronized (this) {
            lastNumber = i;
            lastFactors = factors.clone();
        }
    }
    encodeIntoResponse(resp, factors);
}

据我所知,这本书的作者也说过,如果我们希望一个或多个命令作为一个整体,一个原子发挥作用,则可以同步一个或多个命令。如果同步片段包含多个原子化操作,这是有意义的。

那么,同步

的意义是什么?
return hits;

操作?不是原子的已经吗?

最简单的答案是:如果 getter 未同步,结果将与 nothing 同步时相同。

例如,您可以随时调用 getter,即使另一个线程位于 service 方法内的 synchronized 块的中间。另一个不太明显的结果是 getter 不能保证观察 任何 hits 字段的更新 。 Java 内存模型暗示了这一点,特别是 发生在 写入该字段和 getter 读取之间的关系之前 getter。

该变量不是 volatile,因此某些线程可以读取缓存的(可能是陈旧的)值,除非同步访问。实际上,鉴于 hits 计数器不会用于 做出决定 (即在共享数据结构上写入)而只是为了收集一些统计信息,这没什么大不了的。尽管如此,保护共享变量并确保正确的数据同步是一种很好的做法。

在那种特殊情况下,恕我直言,使用原子整数可能是更好的选择,因为命中次数可能会被某些性能监控线程连续采样,而您只是为了检索一个整数而阻塞了整个数据结构。

互斥的主要原因(即Javasynchronized块提供的)是为了防止其他线程看到不一致的数据一个线程正在更改数据时的状态。为了使其工作,所有 访问数据的线程必须在同一个对象上同步。修改数据的线程必须同步,仅查看数据的线程也必须同步。

你的 getHits() 方法看起来非常简单,也许你想知道它怎么会看到 hits 处于不一致的状态,但 hits 是一个 long。 Java 语言规范允许 long 变量分两步更新,因为在 32 位硬件上,没有其他方法。因此,如果没有同步,在某些硬件上,getHits() 可能会 return 一个从未分配给 hits 的长值。 (即,它可以 return 一个 64 位值,由一次更新的 32 位和另一次更新的 32 位组成)。

通过同步 getHits() 方法 更新 hits 的代码块,您的代码示例可以防止这种情况发生。


同步也做了 gd1 所说的:它可以帮助线程 运行 在一个 CPU 上完成的更新对另一个 CPU 上的线程 运行 可见秒。 Java 语言规范说,无论一个线程在退出 synchronized 块之前在内存中发生什么变化,都必须对另一个线程可见 after 另一个线程随后进入synchronized 阻塞在同一个对象上。

如果不同步会发生什么情况,再次取决于硬件平台。在某些系统上,即使没有同步,更新也会立即对其他线程可见,但在其他系统上,可能需要任意长的时间才能复制数据。