为什么需要同步?

时间:2010-03-10 16:50:37

标签: java multithreading thread-safety

我正在努力填补我的Java线程知识​​中的一些可耻的空白,我正在阅读Brian Goetz等人(强烈推荐的BTW)中的Java Concurrency in Practice,本书的早期例子之一留给我一个题。在下面的代码中,我完全理解为什么在更新hitscacheHits成员变量时需要同步,但为什么只需阅读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);
}...

我感觉它与原子性,监视器和锁定有关,但我不完全理解这些,所以请有人在那里解释一下吗?

提前致谢...

詹姆斯

6 个答案:

答案 0 :(得分:6)

除了在没有同步时,非volatile变量的线程缓存视图可能无限期地保持不更新这一事实:

<强> read/write access to a long or double is NOT guaranteed to be atomic!

理论上,您最终可以看到只更新第一个或最后4个字节的值。但是,volatile也解决了这个问题。

答案 1 :(得分:3)

因为否则你可能会看到一个陈旧的命中值。挥发性也会起作用。这是相关的http://www.ibm.com/developerworks/java/library/j-jtp04186/index.html

答案 2 :(得分:1)

基本的经验法则是,当您需要对变量进行同步访问时,您始终需要读取和写入。否则,一个线程更新的值有可能永远不会被其他线程看到......

答案 3 :(得分:1)

因为匹配未标记为易失性。如果要访问可以更改为多个线程的变量(如下所示),则必须将其标记为volatile或在同步块中访问它。

编辑:正如在此处所述并在Effective Java书中举例说明的那样,如果您不通过synchronized块访问它,则无法看到另一个线程更改的值。请注意,这种情况正在发生,因为该变量不是最终的,并且它不是并发包中的包装器之一。如果它是final和其中一个变量,则不需要同步块。

......另一种选择是使用AtomicLong,使其成为最终版。该变量不必是volatile,因为变量不是变化,只是其内容,由AtomicLong类管理。

答案 4 :(得分:1)

这里有许多潜在的问题。迈克尔指出了一个大的(长商店的非原子性),但还有另一个。如果没有'发生在'之前的关系(在释放和获取锁之间提供,例如在synchronized块中),则可以看不到写入。

请注意,++hits行位于++cacheHits中的service()之前。在没有synchronized的情况下,JVM完全有权以一种可能使其他线程混淆的方式重新排序这些指令。例如,它可能会在<{em> ++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%的缓存命中率可能不太明显,对穷人来说更加困惑调试器。

同步读取会设置一个先发生的关系,这样就不会发生这种情况,然后锁定会提供其他人提到的其他优点。

答案 5 :(得分:0)

如果你担心代码的性能(这是我的猜测),你应该知道(1)稳定性优于并发应用程序中的速度,以及(2)像Sun这样的其他人也有同样的关注因此设计了这个:

ReentrantReadWriteLock