Java线程死锁由互斥、不可抢占、请求保持和循环等待四个条件引发,导致线程永久阻塞(如线程A持锁1等锁2,线程B持锁2等锁1),可通过统一锁获取顺序、tryLock超时机制和减少锁粒度预防;活锁则因线程过度协调(如双方不断重试释放锁)导致持续忙碌但系统无进展,需通过随机等待时间或指数退避算法解决。死锁检测依赖jstack(输出"DEADLOCK")或VisualVM可视化工具,活锁需分析日志和监控CPU高占用却无有效进展的特征。预防核心在于设计阶段统一锁顺序、避免嵌套锁、设置合理超时,运行时实施监控(如Arthas线程分析)和合理重试策略,确保高并发系统(如电商订单、实时交易)的稳定性和响应性。

一、死锁(Deadlock)

1. 死锁概念与产生条件

死锁是指两个或多个线程互相持有对方所需要的资源,都在等待对方执行完毕才能继续往下执行的状态,导致所有线程都陷入无限等待中。

死锁的四个必要条件(必须同时满足才会发生死锁):

  1. 互斥使用:资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
  3. 请求和保持:当资源请求者在请求其他资源的同时保持对原有资源的占有
  4. 循环等待:存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,形成等待环路

2. 死锁的典型场景与代码示例

错误示例(导致死锁)

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public void thread1Task() {
        synchronized (lock1) {
            System.out.println("Thread1: Holding lock1, waiting for lock2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Thread1: Holding lock1 and lock2");
            }
        }
    }

    public void thread2Task() {
        synchronized (lock2) {
            System.out.println("Thread2: Holding lock2, waiting for lock1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock1) {
                System.out.println("Thread2: Holding lock2 and lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();
        new Thread(example::thread1Task).start();
        new Thread(example::thread2Task).start();
    }
}

运行结果:程序将永久阻塞,无法继续执行。

3. 死锁检测方法

3.1 命令行方式

# 1. 查看Java进程ID
jps

# 2. 查看特定进程的线程状态
jstack <进程ID>

在输出中,如果出现"DEADLOCK"字样,即表示存在死锁。

3.2 可视化工具

  • VisualVM

    1. 启动VisualVM并连接目标JVM
    2. 点击"线程"选项卡
    3. 在右上角过滤器中选择"检测死锁"功能
    4. 查看自动检测到的死锁线程组
    5. VisualVM会以图形方式展示线程之间的锁依赖关系
  • Java Mission Control (JMC)

    • 提供"锁竞争分析"功能,可生成火焰图,直观显示哪些锁被频繁争用及等待时间分布

4. 死锁解决方法

4.1 统一锁获取顺序(推荐方法)

破坏"循环等待"条件,确保所有线程按照相同顺序获取锁。

public class DeadlockPrevention {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    // 所有线程按lock1 -> lock2的顺序获取锁
    public void task() {
        synchronized (lock1) {
            System.out.println("Thread: Holding lock1, waiting for lock2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Thread: Holding lock1 and lock2");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockPrevention example = new DeadlockPrevention();
        new Thread(example::task).start();
        new Thread(example::task).start();
    }
}

适用场景:大多数需要获取多个锁的业务场景,特别是订单处理、库存管理等需要保证操作原子性的系统。

4.2 使用超时获取锁(tryLock)

破坏"持有并等待"条件,避免线程无限等待。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockExample {
    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    public void task() {
        boolean hasLock1 = false;
        boolean hasLock2 = false;
        try {
            // 尝试获取lock1,超时1秒
            hasLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
            if (hasLock1) {
                System.out.println("获取lock1成功,尝试获取lock2");
                // 尝试获取lock2,超时1秒
                hasLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
                if (hasLock2) {
                    System.out.println("获取lock2成功,执行任务");
                } else {
                    System.out.println("获取lock2超时,释放lock1");
                }
            } else {
                System.out.println("获取lock1超时");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 释放已获取的锁
            if (hasLock2) lock2.unlock();
            if (hasLock1) lock1.unlock();
        }
    }

    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();
        new Thread(example::task).start();
        new Thread(example::task).start();
    }
}

适用场景:对响应时间有要求的系统,如Web服务、实时交易系统。

4.3 减少锁粒度

将大锁拆分为多个小锁,降低锁竞争。

public class ReducedLockGranularity {
    private final Object orderLock = new Object();
    private final Object inventoryLock = new Object();
    
    public void createOrder(String userId, String productId) {
        synchronized (orderLock) {
            // 创建订单
        }
        synchronized (inventoryLock) {
            // 更新库存
        }
    }
    
    public void cancelOrder(String orderId) {
        synchronized (orderLock) {
            // 取消订单
        }
        synchronized (inventoryLock) {
            // 恢复库存
        }
    }
}

适用场景:高并发系统,如电商网站的订单和库存处理。

5. 死锁的预防最佳实践

  1. 统一锁顺序:设计时确保所有线程按相同顺序获取锁
  2. 设置锁超时:使用ReentrantLock的tryLock方法
  3. 避免嵌套锁:尽量将多个锁操作拆分为独立的步骤
  4. 减少锁持有时间:在锁内不要进行耗时操作
  5. 使用更细粒度的锁:将大锁拆分为多个小锁

二、活锁(Livelock)

1. 活锁概念

活锁是指多个线程都在执行各自的操作,但由于彼此间的"协调不当",导致系统整体无法取得进展的状态。与死锁不同,活锁中的线程仍在"忙碌"工作,但整个系统却无法向前推进。

活锁的典型特征

  • 线程持续活动:CPU占用可能很高
  • 无实际进展:系统状态没有实质性变化
  • 无永久阻塞:线程没有被挂起或等待
  • 响应式行为:线程之间相互响应对方的动作

2. 活锁的典型场景与代码示例

错误示例(导致活锁)

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LivelockDemo {
    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            while (true) {
                if (lock1.tryLock()) {
                    System.out.println("ThreadA: 获得lock1");
                    try {
                        Thread.sleep(100); // 模拟工作
                        if (lock2.tryLock()) {
                            try {
                                System.out.println("ThreadA: 获得lock2 - 完成任务");
                                return; // 成功退出
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock1.unlock();
                    }
                }
                System.out.println("ThreadA: 重试中...");
                try {
                    Thread.sleep(50); // 避免CPU过载
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(() -> {
            while (true) {
                if (lock2.tryLock()) {
                    System.out.println("ThreadB: 获得lock2");
                    try {
                        Thread.sleep(100); // 模拟工作
                        if (lock1.tryLock()) {
                            try {
                                System.out.println("ThreadB: 获得lock1 - 完成任务");
                                return; // 成功退出
                            } finally {
                                lock1.unlock();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock2.unlock();
                    }
                }
                System.out.println("ThreadB: 重试中...");
                try {
                    Thread.sleep(50); // 避免CPU过载
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();
        threadB.start();
    }
}

运行结果:两个线程不断重试,但永远无法同时获取两把锁,系统无实际进展。

3. 活锁的检测方法

活锁检测相对困难,因为线程仍在运行。一般通过以下方式检测:

  1. 系统监控:观察系统CPU使用率高但无有效工作进展
  2. 日志分析:分析线程日志,发现线程不断重试但无成功
  3. 线程状态分析:使用jstack查看线程状态,确认线程处于RUNNABLE状态但无实际进展

4. 活锁解决方法

4.1 随机等待时间

在重试前加入随机等待时间,避免线程同时重试。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.ThreadLocalRandom;

public class LivelockFixed {
    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            while (true) {
                if (lock1.tryLock()) {
                    System.out.println("ThreadA: 获得lock1");
                    try {
                        Thread.sleep(100); // 模拟工作
                        if (lock2.tryLock()) {
                            try {
                                System.out.println("ThreadA: 获得lock2 - 完成任务");
                                return; // 成功退出
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock1.unlock();
                    }
                }
                System.out.println("ThreadA: 重试中...");
                // 加入随机等待时间,避免与ThreadB同时重试
                try {
                    Thread.sleep(ThreadLocalRandom.current().nextInt(50, 150));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(() -> {
            while (true) {
                if (lock2.tryLock()) {
                    System.out.println("ThreadB: 获得lock2");
                    try {
                        Thread.sleep(100); // 模拟工作
                        if (lock1.tryLock()) {
                            try {
                                System.out.println("ThreadB: 获得lock1 - 完成任务");
                                return; // 成功退出
                            } finally {
                                lock1.unlock();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock2.unlock();
                    }
                }
                System.out.println("ThreadB: 重试中...");
                // 加入随机等待时间,避免与ThreadA同时重试
                try {
                    Thread.sleep(ThreadLocalRandom.current().nextInt(50, 150));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();
        threadB.start();
    }
}

适用场景:需要避免线程同时重试的场景,如网络请求重试、分布式系统资源竞争。

4.2 指数退避算法

在重试前等待更长时间,随着重试次数增加等待时间指数增长。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ExponentialBackoff {
    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();
    
    public void task() {
        int retryCount = 0;
        while (true) {
            if (lock1.tryLock()) {
                try {
                    if (lock2.tryLock()) {
                        try {
                            System.out.println("成功获取锁,执行任务");
                            return;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }
            
            // 指数退避
            int delay = (int) Math.pow(2, retryCount) * 100;
            System.out.println("重试中,等待" + delay + "ms");
            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            retryCount++;
        }
    }

    public static void main(String[] args) {
        ExponentialBackoff example = new ExponentialBackoff();
        new Thread(example::task).start();
        new Thread(example::task).start();
    }
}

适用场景:高频率重试场景,如网络请求、数据库连接重试。

5. 活锁的预防最佳实践

  1. 避免过度协调:不要设计过于"礼貌"的线程协调逻辑
  2. 引入随机性:在重试前加入随机等待时间
  3. 设置最大重试次数:避免无限重试
  4. 改变协调逻辑:避免线程之间过度依赖对方的状态
  5. 监控系统状态:通过监控工具及时发现活锁问题

三、死锁与活锁对比总结

特性死锁活锁
线程状态完全阻塞持续运行
系统进展无进展无有效进展
CPU使用率
检测难度相对容易(jstack标注DEADLOCK)较难(需要分析日志和线程行为)
解决思路破坏死锁条件打破过度协调循环
典型场景锁顺序不一致过度礼貌的协调逻辑

四、多线程问题排查与预防最佳实践

1. 代码编写阶段

  • 统一锁顺序:所有线程按相同顺序获取锁
  • 使用tryLock:设置合理的超时时间
  • 减少锁持有时间:在锁内避免耗时操作
  • 使用细粒度锁:避免大锁竞争
  • 避免嵌套锁:尽量将多个锁操作拆分为独立步骤

2. 运行时监控

  • 使用VisualVM/JConsole:监控线程状态和锁竞争
  • 设置JVM参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintGCApplicationStoppedTime 等
  • 定期生成线程快照:jstack定期输出线程状态
  • 监控CPU使用率:发现活锁的早期迹象

3. 生产环境最佳实践

  1. 设置合理的超时:对关键操作设置合理的超时时间
  2. 实现重试机制:对可重试操作实现指数退避重试
  3. 日志记录:记录锁获取和释放的关键日志
  4. 监控系统:实现对线程状态的实时监控
  5. 定期检查:定期检查代码中可能的锁竞争点

4. 高级排查技巧

  • Arthas:使用Arthas的thread命令查看线程状态

    arthas> thread -n 3
    
  • 火焰图分析:使用Java Mission Control生成锁竞争火焰图

  • 线程转储分析:分析jstack输出,寻找"waiting for monitor entry"等关键信息


五、总结

Java线程死锁和活锁是并发编程中的常见问题,但通过正确的设计和预防措施,可以有效避免这些问题。

死锁:通过统一锁顺序、使用tryLock、减少锁粒度等方法预防。
活锁:通过引入随机等待时间、指数退避算法等方法预防。

关键原则

  1. 设计阶段预防:在编写代码前考虑并发问题
  2. 运行时监控:及时发现潜在问题
  3. 代码实践:遵循最佳实践编写线程安全代码

记住:"预防胜于治疗"。在并发编程中,提前考虑可能的并发问题比在出现问题后修复要高效得多。通过合理的设计和充分的测试,可以大幅降低死锁和活锁的发生概率,提高系统的稳定性和可靠性。