《Java并发编程实战》读书笔记:第10章 避免活跃性风险
引言:安全性与活跃性的权衡
在并发编程中,我们常常过度关注线程安全性(Thread Safety),却忽略了过度安全可能导致的活跃性(Liveness)问题。活跃性问题表现为程序崩溃、停滞或运行过慢,但不一定产生错误结果。本章深入探讨了Java并发中最常见的活跃性风险——死锁(Deadlock),以及其他相关的活跃性危害。
死锁:并发编程的严重问题
死锁的基本概念
死锁是指两个或多个线程永远相互等待对方释放锁资源的情况。最简单的死锁场景可以用哲学家就餐问题(Dining Philosophers Problem)来类比:
锁顺序死锁(Lock-ordering Deadlocks)
// 警告:容易发生死锁!
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
synchronized (right) {
synchronized (left) {
doSomethingElse();
}
}
}
}
死锁发生条件:
- 互斥条件:资源一次只能被一个线程占用
- 持有并等待:线程持有资源并等待其他资源
- 不可抢占:资源不能被强制抢占
- 循环等待:存在循环等待链
动态锁顺序死锁
当锁对象是动态参数时,死锁风险更加隐蔽:
// 警告:容易发生死锁!
public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
死锁场景:
// 线程A
transferMoney(myAccount, yourAccount, 10);
// 线程B
transferMoney(yourAccount, myAccount, 20);
死锁预防与解决方案
一致的锁顺序策略
private static final Object tieLock = new Object();
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) {
class Helper {
public void transfer() throws InsufficientFundsException {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (toAcct) {
synchronized (fromAcct) {
new Helper().transfer();
}
}
} else {
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
开放调用(Open Calls)原则
问题代码:
class Taxi {
private final Dispatcher dispatcher;
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination))
dispatcher.notifyAvailable(this); // 持有锁时调用外部方法
}
}
class Dispatcher {
public synchronized Image getImage() {
for (Taxi t : taxis)
image.drawMarker(t.getLocation()); // 持有锁时调用外部方法
return image;
}
}
解决方案:使用开放调用
@ThreadSafe
class Taxi {
public void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination)
dispatcher.notifyAvailable(this); // 开放调用
}
}
@ThreadSafe
class Dispatcher {
public Image getImage() {
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t : copy)
image.drawMarker(t.getLocation()); // 开放调用
return image;
}
}
其他活跃性危害
饥饿(Starvation)
| 原因 | 表现 | 解决方案 |
|---|---|---|
| 线程优先级不当 | 低优先级线程永远得不到执行 | 避免使用线程优先级 |
| 非终止结构 | 无限循环或资源等待 | 使用超时机制 |
| 锁持有时间过长 | 其他线程无法获取资源 | 减小同步块范围 |
响应性差(Poor Responsiveness)
活锁(Livelock)
活锁发生时线程并未阻塞,但无法取得进展:
// 活锁示例:消息处理系统
while (true) {
try {
processMessage(message);
break;
} catch (ProcessingException e) {
// 消息回滚到队列头部,导致无限重试
messageQueue.addFirst(message);
Thread.sleep(100);
}
}
解决策略:引入随机退避机制
// 使用随机退避避免活锁
int attempt = 0;
while (true) {
try {
processMessage(message);
break;
} catch (ProcessingException e) {
long backoffTime = (long) (Math.random() * Math.pow(2, attempt) * 100);
Thread.sleep(backoffTime);
attempt++;
if (attempt > MAX_ATTEMPTS) {
// 移至死信队列
deadLetterQueue.add(message);
break;
}
}
}
死锁诊断与分析
线程转储(Thread Dump)分析
# 生成线程转储
kill -3 <process_pid>
# 或 Ctrl+\ (Unix) / Ctrl+Break (Windows)
线程转储示例输出:
Found one Java-level deadlock:
=============================
"ApplicationServerThread":
waiting to lock monitor 0x080f0cdc (a MumbleDBConnection),
which is held by "ApplicationServerThread"
"ApplicationServerThread":
waiting to lock monitor 0x080f0ed4 (a MumbleDBCallableStatement),
which is held by "ApplicationServerThread"
定时锁尝试(Timed Lock Attempts)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SafeResourceAccess {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public boolean tryAccess(long timeout, TimeUnit unit) {
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (System.nanoTime() < stopTime) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 成功获取两个锁
return true;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
// 短暂的休眠避免忙等待
Thread.sleep(10);
}
return false;
}
}
最佳实践总结
死锁预防策略对比表
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 一致的锁顺序 | 简单有效 | 需要全局分析 | 静态锁对象 |
| 开放调用 | 提高可组合性 | 可能降低原子性 | 协作对象 |
| 定时锁 | 避免无限等待 | 实现复杂 | 高并发环境 |
| 资源排序 | 通用性强 | 可能有性能开销 | 动态资源 |
代码审查清单
- 锁顺序检查:确认所有锁获取是否遵循固定顺序
- 开放调用验证:检查是否在持有锁时调用外部方法
- 超时机制:重要操作是否设置超时限制
- 资源限制:检查资源池大小是否合理
- 线程转储分析:定期进行死锁检测
性能与安全权衡
结论
避免活跃性风险是并发编程中的高级技能,需要在安全性、性能和活跃性之间找到最佳平衡点。通过遵循一致的锁顺序策略、采用开放调用原则、使用定时锁机制和定期进行线程转储分析,可以显著降低死锁和其他活跃性问题的发生概率。
记住:预防胜于治疗。在代码设计阶段就考虑活跃性风险,远比在生产环境中处理死锁要容易得多。
关键收获:
- 死锁是活跃性问题的首要威胁
- 开放调用大幅降低死锁风险
- 定时锁提供死锁逃生机制
- 线程转储是诊断死锁的利器
- 在安全性和活跃性之间需要明智权衡
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



