活跃度与性能 Synchronized

本文探讨了如何通过调整synchronized块的范围,在确保线程安全的同时提高Servlet的并发处理能力。介绍了CachedFactorizer的设计,该设计通过减少锁持有的时间来改善性能。
在UnsafeCachingFactorizer中,我们引入一些缓存到factoring servlet中,希望提高它的性能。缓存需要一些共享状态,需要依次同步来维护这些状态的完整性。但是,在SynchronizedFactorizer中,Serlvet在我们的同步下运行,性能变得很糟糕。SynchronizedFactorizer的同步策略是:用Servlet对象的内部锁保护每一个状态变量。这
图2.1  SynchornizedFactorizer的弱并发
个策略是通过同步整个Service方法实现的。这种简单粗糙的方法虽然使我们重获安全性,但是代价高昂。
Service方法声明为Synchronized,因此每次只能有一个线程执行它。这违背了Serlvet框架的使用初衷——Serlvet可以同时处理多个请求——并且当负载过高时会引起用户的不满。如果Servlet正忙于处理一个大数的因式分解,那么在它可以处理一个新的运算开始前,其他用户必须等待,直到当前的请求完成。这种情况下,在多CPU系统中,即使负载很高,仍然会有处理器处于空闲。无论如何,即使是运行时间短的请求,比如请求缓存的值,仍然可能耗费难以预期长的时间,因为它们必须等待前一个耗时的请求完成。
图2.1演示了多个请求到达同步的Factoring Servlet时所发生的事情:这些请求排队等候并依次被处理。我们把这种Web应用的运行方式描述为弱并发(poor concurrency)的一种表现:限制并发调用数量的,并非可用的处理器资源,而恰恰是应用程序自身的结构。幸运的是,通过缩小synchronized块的范围来维护线程安全性,我们很容易提升Servlet的并发性。你应该谨慎地控制synchronized块不要过小;你不可以将一个原子操作分解到多个synchronized块中。不过你应该尽量从synchronized块中分离耗时的且不影响共享状态的操作。这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。
清单2.8的CachedFactorizer重新构造了servlet:使用两个分离的synchronized块,每个都限制在很短的代码段中。第一个synchronized块保护着检查再运行的操作,以检查我们是否可以返回缓存的结果;另一个保证缓存number和factors的同步更新。另外我们还重新引入了命中计数以及一个“cache hit”计数器,并在第一个的synchronized块内更新它们。由于这两个计数器构成了共享的、可变的状态,我们必须在访问它们的每处都使用同步。synchronized块之外的代码独享地操作本地(基于栈的)变量,这些
变量不被跨线程地共享,因此不需要同步。
清单2.8  缓存最新请求和结果的servlet
@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没有使用AtomicLong类型的命中计数,而是又重新使用了一个long类型的域。在这里使用AtomicLong是安全的,但是却得不到比在CountingFactorizer中更多的好处。原子变量可以保证单一变量的操作是原子的,然而我们已经使用synchronized块构造了原子操作。使用两种不同的同步机制会引起混淆,而且性能与安全也不能从中得到额外的好处。
重新构造后的CachedFactorizer提供了简单性(同步整个方法)与并发性(同步尽
可能短的代码路径)之间的平衡。请求与释放锁的操作需要开销,所以将synchronized块分解得过于琐碎(比如将++hit分解到它自身的synchronized块中)是不合理的,即使这样做是为了获得更好的原子性。当访问状态变量或者执行复合操作期间,CachedFactorizer会占有锁,但是执行潜在耗时的因数分解之前,它会释放锁。这样既保护了线程安全性,也不会过多地影响并发性;每个synchronized块的代码路径已经“足够短”了。
决定synchronized块的大小需要权衡各种设计要求,包括安全性(这是我们决不能妥协的)、简单性和性能。有时简单性与性能会彼此冲突,然而正如CachedFactorizer示范的那样,通常都能够从中找到一个合理的平衡。
通常简单性与性能之间是相互牵制的。实现一个同步策略时,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协)。
当使用锁的时候,你应该清楚块中的代码的功能,以及它的执行过程是否会很耗时。无论是作运算密集型的操作,还是在执行一个可能存在潜在阻塞的操作,如果线程长时间地占有锁,就会引起活跃度与性能风险的问题。
有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值