第二章 线程安全
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的时候,其他线程也不可能接触到其余变量。
保证线程安全的覆盖标准是:对于会导致状态变化的变量,在读、写他们的地方都要加上保护。