2 线程安全
2.1何为线程安全
对象通常具有状态。对象的状态包括任何能改变其外部行为的数据。例如,HashTable的状态包括HashTable自身的数据和其内部Map.Entry中的数据。对象的状态被存储在状态变量中(包括子对象和静态数据成员等)。编写线程安全代码的核心就在于控制多个线程对状态变量的访问。确保状态变量在多线程间被正确访问的途径如下:- 不在线程间共享状态变量
- 使用不可变的(immutable)状态变量,或者
- 对共享可变的状态变量的访问进行同步
无状态的对象总是线程安全的,如程序2-1中的StatelessFactorizer。有状态的对象是否需要线程安全取决于它是否会被多个线程同时访问(而非用它做什么)。
线程安全类的定义:A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving[交叉] of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
程序2-1 无状态的StatelessFactorizer提供求因子服务,线程安全
安全的关键在于正确。
If an object is correctly implemented
- no sequence of operations calls to public methods and reads or writes of public fields should be able to violate any of its invariants or postconditions.
- no set of operations performed sequentially or concurrently on instances of a thread-safe class can cause an instance to be in an invalid state.
线程安全的程序可能包含非线程安全类。全部由线程安全类构成的程序不一定线程安全。
本章中后续提到的状态变量默认为是共享、可变的状态变量。
2.2原子性
在StatelessFactorizer中添加变量count,记录收到请求的次数。代码如程序2-2.@NotThreadSafe程序2非线程安全。++count操作可能出现第一章中图1所示的错误。 service方法能否正确执行取决于多个线程的执行顺序。这种现象称为Race Condition。没有同步的自增操作属于其中常见的一类:read-modify-write。
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; // unsafe
encodeIntoResponse(resp, factors);
}
}
程序2-2
另一种常见的Race Condition是check-then-act。程序检查判断条件为真,进入if块中,准备执行相应的操作,但在判断之后,执行操作之前,该条件变为假,从而导致错误。例如程序2-3中单件的实现。
@NotThreadSafeinstance初始为空。线程A,B同时执行getInstance()。A先执行1,然后进入if中,但在执行2之前,B执行1,由于A尚未执行2,B也能进入if中。最终A,B将获得不同的instance。
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null) // 1
instance = new ExpensiveObject(); // 2
return instance;
}
}
程序2-3 非线程安全的单件
原子操作:假设有操作A,B,从执行A的线程的角度来看,当其他线程执行B时,要么B全部执行,要么一点没有执行,这样A和B互为原子操作。一个原子操作是指:该操作和所有操作,包括它自己,都互为原子操作。
要消除Race Condition,需要将read-modify-write和check-then-act整个过程的所有操作组合成一个原子复合操作。
在程序2-2中,要实现原子复合操作,可以将count的类型替换为线程安全的AtomicLong。AtomicLong实现了对long类型变量各种复合操作的封装。由于 UnsafeCountingFactorizer只包含count一个状态变量,count线程安全则UnsafeCountingFactorizer线程安全。因此,使用了AtomicLong的UnsafeCountingFactorizer是线程安全的。
对于只有一个状态的类,如果该状态保存在一个线程安全的对象中,那么这个类是线程安全的。
Tips:尽可能使用线程安全的对象(如AtomicLong)保存类的状态。这样利于分析该类的线程安全性。
2.3琐
考虑在Factorizer中加入缓存功能。添加两个状态变量,lastNumber用于记录上一次请求的数字,lastFactors记录该数字的因子。收到请求后,先判断请求的数字是否为lastNumber,是则返回lastFactors。否则计算新数字的因子,然后更新lastNumber和lastFactors的值。lastNumber和lastFactors之间存在不定式:lastFactors一定是lastNumber的因子。程序2-4所示。@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())) // 1
encodeIntoResponse(resp, lastFactors.get() );
else {
BigInteger[] factors = factor(i);
lastNumber.set(i); // 2
lastFactors.set(factors); // 3
encodeIntoResponse(resp, factors);
}
}
}
程序 2-4 带缓存功能的Factorizer虽然UnsafeCachingFactorizer 使用了原子类型的变量来保存状态,但依然非线程安全。因为存在多线程间的一种执行顺序,使lastNumber和 lastFactors中的状态不一致,即破坏了两者之间的不定式。考虑A,B两个线程,同时请求16的因子。UnsafeCachingFactorizer 此时缓存10的因子。假设A先执行1,不命中,执行else语句,在执行2之后,执行3之前,lastNumber被置为16,但lastFactors中依然保存10的因子。此时,B执行1,命中,UnsafeCachingFactorizer直接返回10的因子。
为了保证状态的一致性,不定式关系中的多个状态变量的更新应该在一个原子复合操作中完成。原子类型的变量只能保证一个状态变量的复合操作的原子性。琐则可以将任意多个操作封装成一个原子复合操作。
为了能够保证原子操作,Java提供了内部琐机制:synchronized 块。
- 任何对象都可以作为 synchronized 块的琐。
- 内部琐是一种互斥琐。任意时刻最多只有一个线程可以拥有该琐。
- 内部琐是可重入琐。一个琐关联一个计数器。琐的占有线程再次请求该琐时,计数器加一;退出一个synchronized 块时,计数器减一。计数器为零时琐被释放。
@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-5 使用了内部琐的Factorizer
2.4 用琐来保证状态的线程安全
状态变量上的复合操作必须通过原子化来避免Race Condition。这可以通过对整个复合过程进行加锁来实现。但仅仅保证复合操作的原子性还不够,我们需要对访问状态变量的任何地方都进行同步,不仅仅是写操作,还包括读操作。同步时必须使用同一把琐。对一个对象加锁(获得该对象关联的琐)只能阻止其它线程对其再次加锁,并不能阻止其它线程访问该对象。其他线程仍能执行synchronized块之外的代码。
任何一个状态变量都必须用一把锁保护其线程安全性。
不变式涉及的状态变量需要使用同一把琐来保证其线程安全。
即使将类中每个函数都声明为synchronized,也不能保证它们的复合操作是原子操作。例如以下复合操作
if (!vector.contains(element))
vector.add(element);
contains和add都线程安全,但这里显然存在一个Race Condition。
2.5. Liveness and Performance
程序2-5中的 SynchronizedFactorizer虽然线程安全,但代价巨大。由于service是 synchronized方法,服务器同时只能处理一个请求,而且增加了同步开销,性能比单线程更差,应当缩小 synchronized的范围。修改后的程序如程序2-6所示。@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, i 都为本地变量,不会在线程间共享
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
程序 2-6 更高效的Factorizer
程序2-6重新在Factorizer中添加了统计功能。hits 和 cacheHits 可以使用原子变量来保证线程安全,但为了统一同步机制,这里使用了内部琐。
在多线程编程中,尽量保持程序的简单性,因为这样更容易分析代码的线程安全性,不易出错。不要为了过于追求性能而急于破坏简单性。
避免对可能非常耗时的操作(如网络通信、磁盘i/o)进行加锁。