死锁和活锁是并发编程与分布式系统中的核心活跃性问题它们都会导致系统无法完成有效工作但成因与表现机制不同因此避免策略也各有侧重。以下将从问题定义、成因分析、具体避免方法及代码实践进行详细阐述 。一、 核心概念与区别在探讨避免方法前必须清晰区分死锁与活锁。特征死锁活锁线程状态线程因等待永远不会被释放的资源而完全阻塞处于BLOCKED或WAITING状态。线程未被阻塞仍在运行RUNNABLE但执行的是无效的重试或回退逻辑无法完成工作。资源状态资源被占用且不释放形成循环等待。资源可能被释放和重新获取但在实体间无效传递没有实际工作进展。类比两辆车在单行道上迎面相遇互不相让都停着不动。两个人在走廊相遇同时向同一侧避让结果又挡住了对方不断重复此过程。根本原因四个必要条件同时成立互斥、占有且等待、不可抢占、循环等待。实体间过度协作或冲突解决策略的同步性导致状态在几个非进展点之间循环。二、 死锁的避免与预防策略避免死锁的核心在于破坏其四个必要条件中的至少一个。1. 破坏“占有且等待”要求线程在开始执行前一次性申请其所需的所有资源。若无法一次性获取全部则它不能持有任何已申请的资源。优点逻辑简单。缺点严重降低资源利用率可能造成饥饿且提前预知所有资源需求在实际中往往困难。// 伪代码一次性申请所有锁的转账操作 public void transfer(Account from, Account to, BigDecimal amount) { // 按全局固定顺序如账号ID排序锁对象 Object firstLock (from.id to.id) ? from : to; Object secondLock (from.id to.id) ? to : from; synchronized (firstLock) { synchronized (secondLock) { // 临界区执行实际的转账逻辑 from.withdraw(amount); to.deposit(amount); } } }2. 破坏“不可抢占”允许系统强制剥夺线程已占有的资源。这通常通过支持超时和中断的锁来实现。实现方式使用java.util.concurrent.locks.Lock接口的tryLock()方法并配合随机后退以避免活锁。Java示例import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.Random; public class DeadlockAvoidanceWithTimeout { private final Lock lockA new ReentrantLock(); private final Lock lockB new ReentrantLock(); private final Random random new Random(); public void performOperation() { while (true) { if (lockA.tryLock()) { try { // 尝试获取锁A后带超时尝试获取锁B if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 成功获取两把锁执行业务逻辑 System.out.println(Operation completed successfully.); return; // 成功退出循环 } finally { lockB.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } finally { lockA.unlock(); // 释放锁A允许其他线程抢占 } } // 获取锁失败引入随机后退避免活锁 try { Thread.sleep(random.nextInt(50) 10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } }3. 破坏“循环等待”强制所有线程按全局唯一的固定顺序申请资源。这是最常用且有效的预防策略 。方法为所有可锁对象定义一个全序关系如按对象哈希码、内存地址或业务主键排序。效果任何线程都只能以单向顺序等待资源不可能形成闭环。// 按对象哈希码定义全局锁顺序 public void lockInOrder(Object obj1, Object obj2) { int hash1 System.identityHashCode(obj1); int hash2 System.identityHashCode(obj2); Object first hash1 hash2 ? obj1 : obj2; Object second hash1 hash2 ? obj2 : obj1; synchronized (first) { synchronized (second) { // 执行需要锁保护的代码 doWork(); } } }4. 死锁检测与恢复对于无法完全预防死锁的复杂系统如数据库、操作系统可采用事后检测与恢复机制。检测周期性地构建并分析资源分配图检查是否存在环。恢复线程终止强制终止一个或多个参与死锁的线程。资源抢占从某些线程剥夺资源分配给其他线程需要处理状态回滚和一致性。三、 活锁的避免与解决策略活锁的根源在于行为间的“同步性”和“过度协作”。解决思路是引入随机性、异步性或强制进度。1. 引入随机性当操作如获取锁、发送消息失败时让线程等待一个随机长度的时间后再重试以打破多个线程重试节奏的同步性。应用场景网络协议以太网CSMA/CD、数据库事务重试、锁竞争。Java示例在重试逻辑中结合随机后退。public boolean attemptOperationWithBackoff() { int retries 0; int maxRetries 10; Random rand new Random(); while (retries maxRetries) { if (tryAcquireResource()) { // 尝试获取资源 try { // 执行业务操作 return true; } finally { releaseResource(); } } // 获取失败随机后退 try { long backoffTime (long) (Math.pow(2, retries) rand.nextInt(100)); // 指数退避随机 Thread.sleep(backoffTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } retries; } return false; // 重试多次后失败 }2. 使用先来先服务队列将并发的请求序列化是解决活锁的经典方法 。所有请求进入一个队列由一个协调者按顺序处理消除了决策的同步性。实例线程池的任务队列ExecutorService将提交的Runnable或Callable任务放入队列由工作线程按FCFS顺序取出执行。消息队列如Kafka、RabbitMQ生产者将消息发送到队列消费者按顺序消费天然避免了消费者之间的活锁。代码示例使用LinkedBlockingQueue实现一个简单的任务处理器。import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class FCFSTaskProcessor { private final BlockingQueueRunnable taskQueue new LinkedBlockingQueue(); public FCFSTaskProcessor() { // 启动一个消费者线程按FCFS处理任务 new Thread(() - { while (true) { try { Runnable task taskQueue.take(); // 阻塞直到有任务 task.run(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }).start(); } public void submitTask(Runnable task) { taskQueue.offer(task); // 提交任务到队列 } }3. 限制重试次数与设置优先级为操作设置最大重试上限超过后执行失败处理如记录日志、抛出异常、执行降级策略。或者为不同线程设置不同的初始等待时间或优先级使它们的行为模式差异化。4. 采用更高级的并发控制原语避免直接使用低级的synchronized和自旋锁优先使用java.util.concurrent包中设计完善的工具。ConcurrentHashMap通过分段锁减少锁竞争范围。CyclicBarrier/CountDownLatch协调多个线程的同步点避免不协调的推进。Semaphore控制同时访问特定资源的线程数量。5. 设计无锁或乐观并发控制算法从根本上避免锁的使用从而消除因锁竞争引发的活锁和死锁。无锁编程使用CASCompare-And-Swap操作。Java中的AtomicInteger、AtomicReference等原子类是其典型实现。乐观锁先执行操作在提交时检查数据在此期间是否被其他线程修改如通过版本号或时间戳。若冲突则回滚并重试。// 使用 AtomicReference 实现无锁的栈Treiber Stack避免锁竞争 import java.util.concurrent.atomic.AtomicReference; public class LockFreeStackT { private static class NodeT { final T value; NodeT next; Node(T value) { this.value value; } } private final AtomicReferenceNodeT top new AtomicReference(); public void push(T item) { NodeT newHead new Node(item); NodeT oldHead; do { oldHead top.get(); newHead.next oldHead; } while (!top.compareAndSet(oldHead, newHead)); // CAS操作 } public T pop() { NodeT oldHead; NodeT newHead; do { oldHead top.get(); if (oldHead null) { return null; } newHead oldHead.next; } while (!top.compareAndSet(oldHead, newHead)); // CAS操作 return oldHead.value; } }四、 综合对比与最佳实践问题核心策略具体技术/模式注意事项与场景死锁破坏必要条件1. 固定锁顺序2. 超时锁tryLock3. 死锁检测与恢复固定顺序需全局一致超时锁需配合后退以防活锁检测恢复适用于数据库等复杂系统。活锁打破行为同步1. 随机后退Backoff2. 使用FCFS队列3. 限制重试次数4. 采用无锁数据结构后退时间需合理设置队列可能成为性能瓶颈无锁编程复杂度高。通用预防降低并发冲突1. 缩小锁粒度锁细化2. 缩短锁持有时间开放调用3. 使用线程安全并发容器4. 异步与非阻塞IO避免在持有锁时调用外部方法优先使用不可变对象。最佳实践总结设计阶段预防死锁在系统设计时严格规定并遵守全局的资源锁获取顺序这是最有效、最根本的死锁预防手段 。使用带超时的锁永远不要使用无限等待的锁。使用Lock.tryLock(long time, TimeUnit unit)并为失败定义明确的回退或失败处理逻辑。这同时有助于避免死锁和因立即重试导致的活锁 。重试必须随机化在任何重试逻辑中锁获取、网络请求、事务提交必须加入随机延迟如指数退避算法这是解决活锁问题的黄金法则 。优先使用高级并发工具在Java中应优先考虑使用java.util.concurrent包中的ConcurrentHashMap、ExecutorService、Semaphore等高级抽象而非手动管理synchronized和wait/notify。评估无锁方案对于计数器、状态标志等高争用场景优先考虑使用原子变量AtomicXXX或无锁队列可以从根源上避免锁相关问题。监控与测试利用jstack、jconsole或APM工具监控应用运行状态定期进行高并发压力测试以发现和复现潜在的死锁与活锁场景 。参考来源线程的死锁、活锁和饥饿现象JAVA死锁 如何避免死锁_如何避免死锁和活锁 - simar90% 的人答不完整Java 并发面试题死锁、活锁、饥饿全解析查询 host monitor 时发生内部错误_「C#.NET 拾遗补漏」12死锁和活锁的发生及避免...java避免活锁.死锁的解决,java并发编程(九): 避免活跃性危险php 线程/进程安全等等问题锁竞争、死锁、活锁