我试图填补我的Java线程知识方面的可耻空白,并且我正在阅读Brian Goetz等人(强烈推荐的BTW)的Java Concurrency in Practice,这本书中的早期示例之一使我产生了疑问。在下面的代码中,我完全理解为什么更新hits
和cacheHits
成员变量时需要同步,但是为什么在仅读取getHits
变量时hits
方法需要同步呢?
第2章中的示例代码:
public class CachedFactorizer extends GenericServlet implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
private long hits;
private long cacheHits;
public synchronized long getHits() {
return hits;
}
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);
}...
我感觉这与原子性,监视器和锁有关,但是我不完全了解这些,所以请有人在那里进一步解释一下吗?
提前致谢...
詹姆士
最佳答案
这里有多个潜在问题。迈克尔指出了一个大问题(长期商店的非原子性),但还有另一个。在没有``先有先后''的关系(例如在synchronized
块中释放锁和获取锁之间提供)的情况下,可以看到写入顺序困惑。
请注意,++hits
行在++cacheHits
中的service()
之前。在没有synchronized
的情况下,JVM完全有权以一种看起来与其他线程混淆的方式对这些指令进行重新排序。例如,它可以在++cacheHits
之前重新排序++hits
,或者可以在cacheHits
增加值之前使hits
的增加值对其他线程可见(在这种情况下,区别并不重要,因为结果可能相同) 。想象一下,从干净的缓存开始进行重新排序,这将导致以下交错:
Thread 1 Thread 2
--------------- ----------------
++cacheHits (reordered)
cacheHits=1, hits=0
read hits (as 0)
++hits
cacheHits=1, hits=1
read cacheHits (as 1)
calculate 1 / 0 (= epic fail)
您肯定不会得到预期的结果。
提醒您,这很容易调试。您可能有1000次
service()
调用,然后读取线程将cacheHits
视为500,并将hits
视为1。50,000%的高速缓存命中率可能不太明显,甚至使较差的调试器更加困惑。同步读取可建立事前发生的关系,这样就不会发生这种情况,然后锁定提供了其他人提到的其他优点。