@ThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong( 0 );
public long getCount() { return count.get() ;}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}
当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。当类中含有多个状态时,是否只需要添加多个线程安全状态变量就够了?
以下例子假设要为Servlet的因式分解提高性能,当两个连续的请求对相同的数值进行因式分解时,可以直接使用上一次的计算结果,而无须重新计算。
@NotThreadSafe
public class UnsafeCacheingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
public long getCount() { return count.get() ;}
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,lastFactors.get());
}
}
}
UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。只有确保了这个不变性条件不被破坏,上面的Servlet才是正确的。而事实的情况是,尽管set方法是原子的,但仍然无法同时更新lastNumber和lastFactors,同样,我们也不能保证会同时获取两个值。
当类中含有多个状态变量时,应保证这些状态变量的一致性。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。这时就需要用到锁。
内置锁:synchronized方法以class对象作为锁。每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或监视锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时会释放锁。Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁。
使用synchronized修改上面例子:
@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
@GuardedBy("this") private final BigInteger lastNumber;
@GuardedBy("this") private final 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);
}
}
}
这里虽然保证线程安全性,但是并发性却降到了极致,一会我们将继续进行改进。
重入:在java中,内置的锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。"重入"意味着获取锁的操作的粒度是"线程",而不是"调用"。
例:如果内置锁不是可重入的,那么这段代码将发生死锁
public class Widget {
public synchronized void doSomething() {
.....
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
......
super.doSomething();
}
}
用锁来保护状态:
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护,使得其他线程来破坏这里完整性条件变得不可能。
活跃性与性能:
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁。
通常我们可以通过缩小同步代码来提高程序的并发性,但判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性(这个需求必须要得到满足)、简单性和性能。
通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
在我们之前使用的SynchronizedFactorizer类中,Service是一个synchronized方法,因此每次只有一个线程可以执行,这样就违背了Servlet框架的初衷,并发性为0,就算存在多个cpu,后一个servlet请求也得等着前一个请求完成才能进行下一步。
下面我们将对同步代码块减小范围,将计算步骤独立于同步之外,并且利用同一个锁来保护类的不变性条件。
@ThreadSafe
public class CachedFactorizer implements Servlet {
@GuardedBy("this") private final BigInteger lastNumber;
@GuardedBy("this") private final BigInteger[] lastFactors;
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized(this) {
if(i.equals(lastNumber)) {
factors = lastFactors.clone();
}
}
if(factors == null){
BigInteger[] factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp,lastFactors);
}
}