在Goetz and Co着名的“Java并发实践”一书中,在一个“好”的例子中,我发现了以下内容:
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;
操作?是不是原子已经?
答案 0 :(得分:5)
最简单的答案是:如果getter未同步,结果将与 nothing 同步时的结果相同。
例如,您可以随时调用getter,即使另一个线程位于synchronized
方法内service
块的中间。另一个不太明显的结果是,不能保证getter会在hits
字段的所有上观察任何更新。 Java内存模型暗示了这一点,特别是在写入该字段和读取器读取之间关系之前没有发生任何。
答案 1 :(得分:2)
互斥的主要原因(即,Java synchronized
块提供的内容)是防止其他线程在一个线程正在更改数据时看到处于不一致状态的数据。为了使其工作,访问数据的所有线程必须在同一对象上同步。修改数据的线程必须同步,并且外观的线程也必须同步。
您的getHits()
方法看起来非常简单,也许您想知道它如何看到hits
处于不一致状态,但hits
是long
。 Java语言规范允许long
变量分两步更新,因为在32位硬件上没有别的办法。因此,如果没有同步,在某些硬件上,getHits()可能会返回一个从未分配给hits
的long值。 (即,它可以返回一个64位值,该值由一次更新的32位和另一次更新的32位组成)。
通过同步getHits()
方法和更新hits
的代码块,您的代码示例可以防止这种情况发生。
同步也可以执行gd1所说的内容:它可以帮助在一个CPU上运行的线程完成更新,使其在其他CPU上运行的线程可见。 Java语言规范说,在从synchronized
块退出之前,内存在内存中发生的任何变化必须在之后对另一个线程可见,其他线程随后进入synchronized
块同一个对象。
如果您不进行同步,会发生什么情况再次依赖于硬件平台。在某些系统上,即使没有同步,更新也会立即对其他线程可见,但在其他系统上,在复制数据之前可能需要很长时间。
答案 2 :(得分:0)
变量不是volatile
,因此某些线程可以读取缓存(可能是陈旧的)值,除非访问是同步的。实际上,考虑到hits
计数器不会用于做出决策(即在共享数据结构上写)而只是为了收集一些统计数据这一事实,它不是一个大问题。然而,优良作法是保护共享变量并确保正确的数据同步。
在这种特殊情况下,使用原子整数可能是更好的选择IMHO,因为某些性能监视线程可以连续采样命中数,并且只是为了检索整数而阻塞整个数据结构。