线程锁

  通过使用线程安全对象来管理Servlet的全部状态,可以维护Servlet的线程安全性,这样我们只能在Servlet中加入一个状态变量。但是我们如果想加入更多的状态,可以仅
仅加入更多的线程安全的状态变量吗?
想象下面的情形:我们缓存最新的计算结果,以应对两个连续的客户请求相同的数字进行因数分解,希望由此提高Servlet的性能。(这未必是一个有效的缓存策略;在5.6节我们会提供一个更好的。)要实现这个策略,我们需要记住两件事:最新请求的数字和它的因数。
我们在前面曾经是用AtomicLong,以线程安全的方式管理计数量的状态;我们还可以使用同系的AtomicReference6类型管理缓存的数字和它的因数吗?清单2.5中的UnsafeCachingFactorizer作了这种尝试:
清单2.5  没有正确原子化的Servlet试图缓存它的最新结果(不要这样做)
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
     private final AtomicReference<BigInteger> lastNumber
         = new AtomicReference<BigInteger>();
     private final AtomicReference<BigInteger[]>  lastFactors
         = new AtomicReference<BigInteger[]>();
     public void service(ServletRequest req, ServletResponse resp) {
         BigInteger i = extractFromRequest(req);
         if (i.equals(lastNumber.get()))
             encodeIntoResponse(resp,  lastFactors.get() );
         else {
             BigInteger[] factors = factor(i);
             lastNumber.set(i);
             lastFactors.set(factors);
             encodeIntoResponse(resp, factors);
         }
     }
}
很不幸,这种方法并不正确。尽管原子引用(atomic references)自身是线程安全的,不过UnsafeCachingFactorizer中存在竞争条件,导致它会产生错误的答案。
线程安全性的定义要求无论是多线程中的时序或交替操作,都要保证不破坏那些不变约束。UnsafeCachingFactorizer的一个不变约束是:缓存在lastFactors中的各个因子的乘积应该等于缓存在lastNumber中的数值。只有遵守这个不变约束,我们的Servlet才是正确的。当一个不变约束涉及多个变量时,变量间不是彼此独立的:某个变量的值会
制约其他几个变量的值。因此,更新一个变量的时候,要在同一原子操作中更新其他几个。
在一些特殊的时序中,UnsafeCachingFactorizer可能破坏这一不变约束。即使是用原子引用,并且每个set调用都是原子的,我们也无法保证会同时更新lastNumber和lastFactors;当某个线程只修改了一个变量而另一个还没有开始修改时,其他线程将看到Servlet违反了不变约束,这样会形成一个程序漏洞。类似地,也不能保证每个线程都会同时获得两个值:当线程 A尝试获取两个值的时间里,线程 B可能已经修改了它们,线程 A过后会观察到Servlet违反了不变约束。
为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。
2.3.1  内部锁
Java提供了强制原子性的内置锁机制:synchronized块。(第3章将介绍锁和同步机制的另一个重要方面:可见性)一个synchronized块有两部分:锁对象的引用,以及这个锁保护的代码块。synchronized方法是对跨越了整个方法体的synchronized块的简短描述,至于synchronized方法的锁,就是该方法所在的对象本身。(静态的synchronized方法从Class对象上获取锁。)
synchronized (lock) {
  // 访问或修改被锁保护的共享状态
}
每个Java对象都可以隐式地扮演一个用于同步的锁的角色;这些内置的锁被称作内部锁(intrinsic locks)或监视器锁(monitor locks)。执行线程进入synchronized块之前会自动获得锁;而无论通过正常控制路径退出,还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。
内部锁在Java中扮演了互斥锁(mutual exclusion lock,也称作mutex)的角色,意味着至多只有一个线程可以拥有锁,当线程 A尝试请求一个被线程 B占有的锁时,线程 A必须等待或者阻塞,直到 B释放它。如果 B永远不释放锁, A将永远等下去。
同一时间,只能有一个线程可以运行特定锁保护的代码块,因此,由同一个锁保护的synchronized块会各自原子地执行,不会相互干扰。在并发的上下文中,原子性的含义与它在事务性应用中相同——一组语句(statements)作为单独的,不可分割的单元运行。
执行synchronized块的线程,不可能看到会有其他线程能同时执行由同一个锁保护的synchronized块。
同步机制简化了恢复factoring servlet线程安全的工作。清单2.6中,我们将service方法声明为synchronized,所以同一时间内只有一个线程可以进入service方法。现在SynchronizedFactorizer又是线程安全的了;但是这种方法过于极端,它完全禁止多个用户同时使用factoring servlet——这导致糟糕的、令人无法接受的响应性。这个问题——一个性能问题,而非线程安全问题——将在2.5节中深入讨论。
清单2.6  缓存了最新结果的servlet,但响应性令人无法接受(不要这样做)
@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    public synchronized void service(ServletRequest req,
                                     ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber))
            encodeIntoResponse(resp, lastFactors);
        else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp, factors);
        }
    }
}
2.3.2  重进入(Reentrancy)
当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。重进入意味着所的请求是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的7。重进入的实现是通过为每个锁关联一个请求计数(acquisition count)和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同
步块,计数器值将递减。直到计数器达到0时,锁被释放。
重进入方便了锁行为的封装,因此简化了面向对象并发代码的开发。清单2.7中,子类覆写了父类synchronized类型的方法,并调用父类中的方法。如果没有可重入的锁,这段看上去很自然的代码就会产生死锁。因为Widget和LoggingWidget中的doSomething方法都是synchronized类型的,都会在处理前试图获得Widget的锁。倘若内部锁不是可重入的,super.doSomething的调用者就永远无法得到Widget的锁,因为锁已经被占有,导致线程会永久地延迟,等待着一个永远无法获得的锁。重进入帮助我们避免了这种死锁。
清单2.7  如果内部锁不是可重入的,代码将死锁
public class Widget {
    public synchronized void doSomething() {
        ...
    }
}
public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值