非线程安全的数值序列生成器
@NotThreadSafe
public class UnsafeSequence {
private int value;
/**
* Returns a unique value.
*/
public int getNext() {
return value++;
}
}
线程安全的数值序列生成器
@ThreadSafe
public class Sequence {
@GuardedBy("this") private int nextValue;
public synchronized int getNext() {
return nextValue++;
}
}
框架通过在框架线程中调用应用程序代码将并发性引入到程序中 在代码中将不可避免地访问应用程序状态 因此所有访问这些状态的代码路径都必须是线程安全的
线程安全性
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步 那么程序就会出现错误 有三种方式可以修复这个问题
当设计线程安全的类时 良好的面向对象技术 不可修改性 以及明晰的不变性规范都能起到一定的帮助作用
什么是线程安全性
当多个线程访问某个类时 不管运行时环境采用何种调度方式或者这些线程将如何交替执行 并且在主调代码中不需要任何额外的同步或协同 这个类都能表现出正确的行为 那么就称这个类是线程安全的
在线程安全类中封装了必要的同步机制 因此客户端无须进一步采取同步措施
一个无状态的Servlet
@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
无状态对象一定是线程安全的
原子性
在没有同步的情况下统计已处理请求数量的Servlet(不要这么做)
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet 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);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
竞态条件
首先观察到某个条件为真(例如文件X不存在) 然后根据这个观察结果采用相应的动作(创建文件X) 但事实上 在你观察到这个结果以及开始创建文件之间 观察结果可能变得无效(另一个线程在这期间创建了文件X) 从而导致各种问题(未预期的异常 数据被覆盖 文件被破坏等)
延迟初始化中的竞态条件(不要这么做)
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
class ExpensiveObject { }
复合操作
假定有两个操作A和B 如果从执行A的线程来看 当另一个线程执行B时 要么将B全部执行完 要么完全不执行B 那么A和B对彼此来说是原子的 原子操作是指 对于访问同一个状态的所有操作(包括该操作本身)来说 这个操作是一个以原子方式执行的操作
我们将 先检查后执行 以及 读取 修改 写入 等操作统称为复合操作 包含了一组必须以原子方式执行的操作以确保线程安全性
使用AtomicLong类型的变量来统计已处理请求的数量
@ThreadSafe
public class CountingFactorizer extends GenericServlet 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);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {}
BigInteger extractFromRequest(ServletRequest req) {return null; }
BigInteger[] factor(BigInteger i) { return null; }
}
在实际情况中 应尽可能地使用现有的线程安全对象(例如AcomicLong)来管理类的状态 与非线程安全的对象相比 判断线程安全对象的可能状态及其状态转换情况要更为容易 从而也更容易维护和验证线程安全性
加锁机制
该Servlet在没有足够原子性保证的情况下对其最近计算结果进行缓存(不要这么做)
@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
private final AtomicReference lastNumber
= new AtomicReference();
private final AtomicReference lastFactors
= new AtomicReference();
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);
}
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
要保持状态的一致性 就需要在单个原子操作中更新所有相关的状态变量
内置锁
Java提供了一种内置的锁机制来支持原子性 同步代码块(Synchronized Block)
Java的内置锁相当于一种互斥体(或互斥锁) 这意味着最多只有一个线程能持有这种锁 当线程A尝试获取一个由线程B持有的锁时 线程A必须等待或者阻塞 直到线程B释放这个锁 如果B永远不释放锁 那么A也将永远地等下去
这个Servlet能正确地缓存最新的计算结果 但并发性却非常糟糕(不要这么做)
@ThreadSafe
public class SynchronizedFactorizer extends GenericServlet 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);
}
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
重入
当某个线程请求一个由其他线程持有的锁时 发出请求的线程就会阻塞 然而 由于内置锁是可重入的 因此如果某个线程试图获得一个已经由它自己持有的锁 那么这个请求就会成功
如果内置锁不是可重入的 那么这段代码将发生死锁
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
用锁来保护状态
对于可能被多个线程同时访问的可变状态变量 在访问它时都需要持有同一个锁 在这种情况下 我们称状态变量是由这个锁保护的
每个共享的和可变的变量都应该只有一个锁来保护 从而使维护人员知道是哪一个锁
对于每个包含多个变量的不变性条件 其中涉及的所有变量都需要由同一个锁来保护
活跃性与性能
缓存最近执行因数分解的数值及其计算结果的Servlet
@ThreadSafe
public class CachedFactorizer extends GenericServlet 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);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
通常 在简单性与性能之间存在着相互制约因素 当实现某个同步策略时 一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)
当执行时间较长的计算或者可能无法快速完成的操作时(例如 网络I/O或控制台I/O) 一定不要持有锁