《Java并发编程实战》读书笔记:第10章 避免活跃性风险

《Java并发编程实战》读书笔记:第10章 避免活跃性风险

【免费下载链接】booknotes A collection of my book notes on various computer science books 【免费下载链接】booknotes 项目地址: https://gitcode.com/gh_mirrors/bo/booknotes

引言:安全性与活跃性的权衡

在并发编程中,我们常常过度关注线程安全性(Thread Safety),却忽略了过度安全可能导致的活跃性(Liveness)问题。活跃性问题表现为程序崩溃、停滞或运行过慢,但不一定产生错误结果。本章深入探讨了Java并发中最常见的活跃性风险——死锁(Deadlock),以及其他相关的活跃性危害。

死锁:并发编程的严重问题

死锁的基本概念

死锁是指两个或多个线程永远相互等待对方释放锁资源的情况。最简单的死锁场景可以用哲学家就餐问题(Dining Philosophers Problem)来类比:

mermaid

锁顺序死锁(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();
            }
        }
    }
}

死锁发生条件

  1. 互斥条件:资源一次只能被一个线程占用
  2. 持有并等待:线程持有资源并等待其他资源
  3. 不可抢占:资源不能被强制抢占
  4. 循环等待:存在循环等待链

动态锁顺序死锁

当锁对象是动态参数时,死锁风险更加隐蔽:

// 警告:容易发生死锁!
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)

mermaid

活锁(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;
    }
}

最佳实践总结

死锁预防策略对比表

策略优点缺点适用场景
一致的锁顺序简单有效需要全局分析静态锁对象
开放调用提高可组合性可能降低原子性协作对象
定时锁避免无限等待实现复杂高并发环境
资源排序通用性强可能有性能开销动态资源

代码审查清单

  1. 锁顺序检查:确认所有锁获取是否遵循固定顺序
  2. 开放调用验证:检查是否在持有锁时调用外部方法
  3. 超时机制:重要操作是否设置超时限制
  4. 资源限制:检查资源池大小是否合理
  5. 线程转储分析:定期进行死锁检测

性能与安全权衡

mermaid

结论

避免活跃性风险是并发编程中的高级技能,需要在安全性、性能和活跃性之间找到最佳平衡点。通过遵循一致的锁顺序策略、采用开放调用原则、使用定时锁机制和定期进行线程转储分析,可以显著降低死锁和其他活跃性问题的发生概率。

记住:预防胜于治疗。在代码设计阶段就考虑活跃性风险,远比在生产环境中处理死锁要容易得多。

关键收获

  • 死锁是活跃性问题的首要威胁
  • 开放调用大幅降低死锁风险
  • 定时锁提供死锁逃生机制
  • 线程转储是诊断死锁的利器
  • 在安全性和活跃性之间需要明智权衡

【免费下载链接】booknotes A collection of my book notes on various computer science books 【免费下载链接】booknotes 项目地址: https://gitcode.com/gh_mirrors/bo/booknotes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值