2.3.2 重入
由于Widget和LoggingWidget中的doSomething()都是synchronized方法,所以每个doSomething()方法执行前,都会获取Weight上的锁。 super.doSomething() 会产生锁的重入,若没有重入机制,子类线程会一直阻塞下去。
锁操作的粒度是“线程” 而不是“调用”,某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功,这叫做锁的重入。
2.4 用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
p36 ??
每个共享的或者可变的变量都应该由一个锁来保护,从而使维护人员知道是哪一个锁。
一种常见的加锁约定是:将所有的可变状态都封装在对象内部,并通过对象的内置锁,对所有访问可变状态的代码路径进行同步,使得该对象上不会发生并发访问。但并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要锁的保护。
当类的不变性条件涉及多个变量时,那么还有另一个需求:在不变性条件中的每个变量都必须由同一个锁保护。因此可以在单个原子操作中访问或者更新这些变量,从而保证不变性条件不被破坏。
对于包含多个变量的不变性条件,其中涉及的所有变量都应该由同一个锁来保护。
synchronized 方法并不能滥用,会导致活跃性问题或者性能问题。synchronized 虽然能保证单个操作的原子性(例如单个函数),但是要把多个操作合并成一个符合操作,仍需要其他手段保证原子性。
例如:
if(!vector.contain(e)) //add为原子操作
vector.add(e); //vector为原子操作
//整体并不是原子操作,有线程安全性问题,仍存在竞态条件
2.5 活跃性与性能
简单粗粒度的方法虽然能保证线程安全性,但是付出的代价却很高昂
通过缩小同步代码块的范围,可以既保证Service的并发性,又可以确保线程安全。应该将不影响共享状态并且执行时间比较长的代码块从同步代码块中移除,从而在这些操作的执行过程中,其他线程可以访问共享状态。
package chapter2;
import java.math.BigInteger;
@ThreadSafe
public class CachedFactorizer implements Servlet{
@GuardeBy("this") private BigInteger lastNumber;
@GuardeBy("this") private BigInteger[] lastFactors;
@GuardeBy("this") private long hits;
@GuardeBy("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);
BigInterger[] 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);
}
}
通常在简单性与性能之间之间存在着相互制约的因素。当实现某种同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时(例如 网络I/O,控制台I/O),一定不要持有锁。