@NotThreadSafe
public class UnsafeCachingFactorizer extends HttpServlet {
private static final long serialVersionUID = 1L;
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger []> lastFactors = new AtomicReference<BigInteger []>();
/* (non-Javadoc)
* @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
*/
@Override
public void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
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);
}
}
}
阅读上面的代码后,不难发现,尽管使用了线程安全的状态变量,但依然存在着竞态条件(Race conditions)。
在线程安全性的定义中明确声明,多个线程之间的操作无论采用何种执行时序或交叉操作方式,都要保证不变性(invariants)条件不被破坏。UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的值。只有确保了这个不变性条件不被破坏,程序才是正确的。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
一、内置锁(Intrinsic locks)
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来装饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象为锁。
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic locks)或监视器锁(Monitor Lock)。
Java的内置锁相当于一个互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。
@ThreadSafe
public class SynchronizedFactorizer extends HttpServlet {
// ... ...
public synchronized void service(ServletRequest req, ServletResponse resp) { // ... ...}
}
二、重入(Reentrancy)
public class Widget {
public synchronized void doAnything() { }
}
public class LoggingWidget extends Widget {
@Override
public synchronized void doAnything() {
super.doAnything();
}
}
Reentrancy,意味着获取锁的操作粒度是线程,而不是调用。进一步提升了加锁行为的封装性。
Reentrancy的一种实现方式是:为每个锁关联一个获取计数值和一个所有者线程。
三、用锁来保护状态(Guarding state with locks)
由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中,涉及的所有变量都需要由同一个锁来保护。
四、活跃性与性能(Liveness and performance)
通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(可能会破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。