2.5. Liveness and Performance(存活性和性能)

2.5. Liveness and Performance(存活性和性能)
In UnsafeCachingFactorizer, we introduced some caching into our factoring servlet in the hope of improving performance. Caching required some shared state, which in turn required synchronization to maintain the integrity of that state. But the way we used synchronization in SynchronizedFactorizer makes it perform badly. The synchronization policy for SynchronizedFactorizer is to guard each state variable with the servlet object's intrinsic lock, and that policy was implemented by synchronizing the entirety of the service method. This simple, coarse-grained approach restored safety, but at a high price.
UnsafeCachingFactorizer类中,为了能够改进性能,我们向因式分解servlet中增加了缓存机制。缓存需要状态共享,这就需要同步机制来维护状态的一致性。但是SynchronizedFactorizer中,我们的同步使用方式导致执行性能很差。SynchronizedFactorizer类的同步策略是使用servlet对象的内在锁来进行同步,并且通过把整个service方法变成同步方法来实现。虽然这样做可以很轻易的就达到实现安全的目标,但却付出了极大的代价。
Because service is synchronized, only one thread may execute it at once. This subverts the intended use of the servlet framework that servlets be able to handle multiple requests simultaneously and can result in frustrated users if the load is high enough. If the servlet is busy factoring a large number, other clients have to wait until the current request is complete before the servlet can start on the new number. If the system has multiple CPUs, processors may remain idle even if the load is high. In any case, even short-running requests, such as those for which the value is cached, may take an unexpectedly long time because they must wait for previous long-running requests to complete.
由于service方法是同步的,在同一个时刻只有一个线程可以执行该方法。这就颠覆了servlet框架推荐的使用方式:可以同时处理多个用户请求。在负载很高情况下,这将导致用户体验极差。如果servlet在忙于分解一个很大的数值的时候,另外一个客户端必须等待,直到分解结束才能开始分解下一个请求。如果系统拥有多个cpu的时候,即使负载很高,处理器也可能会出现空闲状态。即使是很简短的请求,比如那些值已经被缓存的情况下,也会等待很长时间,因为他们必须等待前一个请求被执行结束。
图2.1展示了当多个请求到达同步因数分解servlet的时候会发生什么事情,他们将会排队等待处理时序。我们可以把这个web应用作为差并发性的一个典型例证:能够同时处理请求的数量并不是由硬件资源限制的,而是由软件架构本身限制。幸运的是,通过把同步块变窄可以改善该servlet的并发性能。你要注意不要把同步块的范围变得过窄。你可能不会想把一个本来应该是原子的操作分解到多个同步块中。但是尽量将需要长时间执行的操作从同步块中排除出来是非常合乎情理的。这样一来,其他线程就不会被访问该常运行的线程阻止。
Figure 2.1. Poor Concurrency of SynchronizedFactorizer.
CachedFactorizer in Listing 2.8 restructures the servlet to use two separate synchronized blocks, each limited to a short section of code. One guards the check-then-act sequence that tests whether we can just return the cached result, and the other guards updating both the cached number and the cached factors.
As a bonus, we've reintroduced the hit counter and added a "cache hit" counter as well, updating them within the initial synchronized block. Because these counters constitute shared mutable state as well, we must use synchronization everywhere they are accessed. The portions of code that are outside the synchronized blocks operate exclusively on local (stack-based) variables, which are not shared across threads and therefore do not require synchronization.
Listing 2.8中的CachedFactorizer类把servlet重新用同步块划分。每一个限制到一段很短的代码中。其中一个代码块守护用来判断是否只是需要返回被缓存结果的“check-then-act”时序。而另外一个代码块则是用来更新被缓存的数值和因数。另外,我们重新定义了“点击计数”并且增加了一个“缓存计数”,这两个计数将会在静态初始化块中被初始化。由于这两个计数会建立共享的可变状态,因此在他们被访问的任何地方必须使用同步机制。在同步代码块之外的代码部分只会操作本地变量,这种变量不会被线程间共享,因此不需要同步机制。
Listing 2.8. Servlet that Caches its Last Request and Result.
@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; }
    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);
    }
}

CachedFactorizer no longer uses AtomicLong for the hit counter, instead reverting to using a long field. It would be safe to use AtomicLong here, but there is less benefit than there was in CountingFactorizer. Atomic variables are useful for effecting atomic operations on a single variable, but since we are already using synchronized blocks to construct atomic operations, using two different synchronization mechanisms would be confusing and would offer no performance or safety benefit.
CachedFactorizer类不再需要AtomicLong类作为“点击计数”而是采用一个长整型域。这里也可以使用AtomicLong类,但是使用这个类带来的好处没有在CountingFactorizer类中使用这个类明显。Atomic变量类型对于单个变量的原子操作来说是有效率的,但是由于我们已经使用来同步代码块来构建原子请求,那么使用两种同步机制可能会带来不必要的混淆,而不会带来性能和安全性的提高。
The restructuring of CachedFactorizer provides a balance between simplicity (synchronizing the entire method) and concurrency (synchronizing the shortest possible code paths). Acquiring and releasing a lock has some overhead, so it is undesirable to break down synchronized blocks too far (such as factoring ++hits into its own synchronized block), even if this would not compromise atomicity. CachedFactorizer holds the lock when accessing state variables and for the duration of compound actions, but releases it before executing the potentially long-running factorization operation. This preserves thread safety without unduly affecting concurrency; the code paths in each of the synchronized blocks are "short enough".
CachedFactorizer类的重构在并发性和简单性两个方面取得了平衡。请求和释放锁是需要一些负荷的,所以即使不会破坏原子性,也不要把同步块分解的过细。CachedFactorizer类访问状态变量的时候或者访问符合行为的时候会持有锁,会在有可能需要长时间执行的因式分解操作是释放锁。这就在不会影响并发性能的时候保证了线程安全。同步块中的代码长度也就“足够短”了。
Deciding how big or small to make synchronized blocks may require tradeoffs among competing design forces, including safety (which must not be compromised), simplicity, and performance. Sometimes simplicity and performance are at odds with each other, although as CachedFactorizer illustrates, a reasonable balance can usually be found.
具体将同步块变成多长需要在几个同等重要的元素之间:安全、简易性、性能之间作出权衡。尽管可以像CachedFactorizer类中描绘的那样在简易性和性能上可以取得一些平衡但有时候,简易性和性能是不匹配的。
There is frequently a tension between simplicity and performance. When implementing a synchronization policy, resist the temptation to prematurely sacriflce simplicity (potentially compromising safety) for the sake of performance.
简单性和性能上存在某种对立。当实现同步策略的时候,应该避免为了追求性能而牺牲简单性(这有可能会隐藏线程不安全元素)。
Whenever you use locking, you should be aware of what the code in the block is doing and how likely it is to take a long time to execute. Holding a lock for a long time, either because you are doing something compute-intensive or because you execute a potentially blocking operation, introduces the risk of liveness or performance problems.
不管什么时候,如果你需要用到锁,你都应该注意阻塞块中的代码行为以及它们大约的运行时间。不管是在进行集中的计算还是在执行潜在的阻塞操作,长时间的持有锁都会带来存活性和性能问题。
Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as network or console I/O.
应该在网络或者控制台IO过程中,避免长时间的持有锁。


你可能感兴趣的:(thread,框架,servlet,网络应用,performance)