线程安全性
- 一个对象是否需要是线程安全的,取决于它是否被多个践程访问.这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。
- Java中的主要同步机制是关键字 synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显式锁(Explicit Lock)以及原子变量。
- 如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
- 不在线程之间共享该状态变量。
- 将状态变量修改为不可变的变量。
- 在访问状态变量时使用同步。
- 一种正确的编程方法就是:首先便代码正确运行,然后再提高代码的速度。即便如此,只是当性能测试结果和应用需求告诉你必须提高性能,以及测量结果表明这种优化在实际环境中确实能带来性能提升时,才进行优化。
什么是线程安全性
- 当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequet req, ServletResponse resp) {
BigInteger i = extractFormRequest(req);
BigInteger[] factors = foctor(i);
encodeIntoRespnse(resp, factors);
}
}
- 无状态对象一定是线程安全的。
原子性
竞态条件
- 当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气。最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步动作。
示例:延迟初始化中的竞态条件
使用“先检查后执行”的一种常见情况就是延迟初始化。延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance () {
if (instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
在LazylnitRace中包含了一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B同时执行getlnstance。A看到instance为空,因而创建一个新的ExpensiveObject实例。B同样需要判断instance是否为空。此时的instance是否为空,要取决于不可预测的时序,包括线程的调度方式,以及A需要花多长时间来初始化ExpensiveObject并设置instance。如果当B检查时,instance为空,那么在两次调用getlmtance时可能会得到不同的结果,即使getlnstance通常被认为是返回相同的实例。
在UnsafeCountingFactorizer的统计命中计数操作中存在另一种竞态条件。在“读取-修改-写入”这种操作(例如递增一个计数器)中,基于对象之前的状态来定义对象状态的转换。要递增一个计数器,你必须知道它之前的值,并确保在执行更新的过程中没有其他线程会修改或使用这个值。
与大多数并发错误一样,竞态条件并不总是会产生错误,还需要某种不恰当的执行时序。然而,竞态条件也可能导致严重的问题。假定LazylnitRace被用于初始化应用程序范围内的注册表,如果在多次调用中返回不同的实例,那么要么会丢失部分注册信息,要么多个行为对同一组注册对象表现出不一致的视图。如果将UnsafeSequence用于在某个持久化框架中生成对象的标识,那么两个不同的对象最终将获得相同的标识,这就违反了标识的完整性约束条件。
复合操作
- 要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变录,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态。而不是在修改状态的过程中。
- 复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
@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);
}
}
在实际情况中,应尽可能地使用现有的线程安全对象(例如 AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。
加锁机制
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInterger> lastNumber
= new AtomicReference<BigInterger>();
private final AtomicReference<BigInterger[]> lastFactors
= new AtomicReference<BigInterger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInter i = extractFromRequest(req);
if (i.equals(lastNumber.get())) {
encodeIntoResponse(resp, lastFactors.get());
} else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(lastFactors);
encdeIntoResponse(resp, factors);
}
}
}
- 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁
- Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
synchronized (lock) {
// 访问或修改由锁保护的共享状态
}
- 每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出。还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码快或方法。
- Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,知道线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等下去。
- 任何一个执行同步代码快的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码快。
@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInterger[] 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);
}
}
}
这个Servlet能正确地缓存最新的计算结果,但并发性缺非常糟糕。
重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
public class Widget {
public synchronized void doSomething () {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething () {
System.out.println(toString() + ":calling doSomething");
super.doSomething();
}
}
用锁来保护状态
- 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
- 每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
活跃性与性能
- 不良并发应用程序(Poor Concurrency)应用程序:可同时调用的数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。
@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);
}
}
- 通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
- 当执行时间较长的计算或者无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。