java并发编程实战学习 第2章
java并发编程实战学习 第2章第1章 简介第2章 线程安全性什么是线程安全性可以同时被多个线程调用,而调用者无需执行额外的动作。一个无状态的Servlet@ThreadSafepublic class StatelessFactorizer implements Servlet {public void service(ServletRequest req, Serv...
java并发编程实战学习 第2章
第1章 简介
第2章 线程安全性
什么是线程安全性
可以同时被多个线程调用,而调用者无需执行额外的动作。
- 一个无状态的Servlet
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoRespose(resp, factors);
}
}
任何一个线程访问,操作都是正确的,那么这就是线程安全的
无状态对象一定是线程安全的
- 在上面基础上,添加一个计数统计功能
@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;
encodeIntoRespose(resp, factors);
}
}
上面这个例子,在单线程环境中可以正常运行,但是在多线程中,就不是一个线程安全的。问题在++count,包含了三个操作,读取-修改-写入,结果依赖于之前的状态。
在并发编程中,不恰当的执行时序而出现不正确的结果,它叫“竞态条件”。
- 另外一个例子 初始化
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if(instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
也是典型的“先检查后执行”,是非线程安全的。
要修复这一问题,“读取-修改-写入”操作必须是原子性的
@ThreadSafe
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoRespose(resp, factors);
}
}
在实际情况中,应尽可能地使用现有的线程安全对象(例如
AtomicLong
)来管理类的状态。
加锁机制
假设我们希望提升Servlet性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上次结果,无数重新计算。
代码进行修改
@NotThreadSafe
public class UnsafeCachingFactorizer {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())) {
encodeIntoRespose(resp, lastFactors.get());
} else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoRespose(resp, factors);
}
}
}
但是仍然不是线程安全的,因为变量虽然使用了线程安全对象,但是这个例子变量和变量之间并不是独立的。需要保证同时去更新。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
Java提供了一种内置锁的机制来支持原子性:同步代码块(Synchronized Block)。
synchronized (lock) {
//访问或修改由锁保护的共享状态
}
Java的内置锁相当于一种互斥体,最多只有一个线程能持有这个锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A永远等。
- 修改代码
@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)) {
encodeIntoRespose(resp, lastFactors);
} else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoRespose(resp, factors);
}
}
}
变量有关联的逻辑都在service
方法内,而整个service
进行了同步,因此线程安全了。这种修改方法过于极端,造成无法多个客户端进行因数分解。
内置锁时可重入的
如果内置锁不是可重入的,那么这段代码将发生死锁
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": call doSomething");
supper.doSomething();
}
}
用锁来保护状态
对于可能被对多个线程访问的可变状态变量,都需要用锁来保护。
活跃性与性能
- 优化上面Servlet代码
@ThreadSafe
public class CachedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
private long hits;
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;
factor = lastFactors.clone();
}
}
if(factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
不能整个方法笼统的同步,需要一个平衡点。
当执行时间较长的计算或者可能无法快速完成的操作时(例如网络IO,或者控制台IO),一定不要持有锁。
更多推荐
所有评论(0)