第二章 线程安全
2.1 什么是线程安全
定义1:无状态的对象一定是现成安全的。
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
如上所示,无状态是指:状态的瞬间转移全部蕴涵于Local变量中(只存在于本线程的堆栈中),且只能由当前线程来获取。
因此,如果只是Req<----->Resp方式的话,Servlet一般都是线程安全的。但是如果要恢复/记录状态的话,就有可能导致线程安全问题。
2.2 原子性
定义2:对有状态类,如含类变量(Field变量)而言,如果不加控制的话,一般不是线程安全的。
例如下面代码不是线程安全的:
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
上述代码的问题并不在于使用了Field变量,而是使用了Filed变量却不加同步控制。因为++count不是原子操作,这会导致在某些杯具时间条件下产生的竞争条件。
这一类竞争条件叫做check-then-act:例如检查某个变量的数值,然后准备对其进行修改,然而在你检查好之后、修改之前,这个数值却发生了变化,这就可能导致很多问题。
再来看一种lazy-load导致的竞争条件,这是基本所有人都会写错的单件类,因为它不是线程安全的:
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
如上所示,如果线程A在new对象的过程中,尚未赋值给instance之前,B来检查instance。B发现instance是null,也初始化了对象。然后单件类的功能就废了。
对于读-写-读的竞争条件,只要将读取i、自增i都做成原子操作就可以解决线程安全隐患了。
Java提供了AtomLong,即原子封装的Long来解决上述问题,如下述代码所示:
@ThreadSafe
public class CountingFactorizer 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);
}
}
使用原子操作对象,是一种很好的线程安全解决办法。但是对包含两个及以上可导致状态转移的变量情况来说并不成立。
2.3 锁
如果同时有两个变量影响状态的变化,则不一定总是正确的,例如对于一个Cache因式分解的例子:
@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);
}
}
}
尽管lastNumber和lastFactors分别都是原子操作,但无法保证在一个原子操作内同时更新这两个数值。因此仍然不是线程安全的。
Java提供了synchronized关键字来完成同步工作。
synchronized (lock) {
// Access or modify shared state guarded by lock
}
在synchronized中包含的代码在同一时间只能被一个线程执行。
这种锁机制是可重入的:如果某线程尝试获取已经获得的锁,则它将直接获得,而非等待释放。
如果没有重入,下面这种代码将死锁:
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
如果同一线程中调用了子类的someThing,就会死锁。
2.4 用锁守护状态
如果使用synchronized同步变量,则必须在使用该变量的每一处添加synchronized。常见的错误认为:只需要在写的时候同步即可。
另一条规则:如果某一状态改变由多个变量共同作用,则为了同步它们,必须使用同一个锁。即将整体的状态改变放在同一个原子操作内。
此时,函数方式的synchronized能更好地解决这个问题:对于函数式synchronized来说,它使用的锁就是这个Object,因此所有同一对象内的synchronized函数使用同一个锁。
然而,并非把所有函数都声明为synchronized就可以解决线程同步问题。
2.5 活跃度和性能
有的时候,往往要在性能和安全性之间选择做出平衡,如果对service全部synchronized,并发性会非常差尽管它是线程安全的,主要通过缩小synchronized的方法在保证线程安全的基础上提升并发性能,如下:
@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);
}
}
在上面的代码中,lastNumber、lastFactors、hints、cacheHits共同导致状态的变化,因此使用同一个锁:this(该对象)将他们保护起来。这样例如在修改lastNumber的时候,其他线程也不可能接触到其余变量。
保证线程安全的覆盖标准是:对于会导致状态变化的变量,在读、写他们的地方都要加上保护。