首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java并发锁的隐藏陷阱:ReentrantLock与Condition的误用导致死锁

Java并发锁的隐藏陷阱:ReentrantLock与Condition的误用导致死锁

原创
作者头像
用魔法才能打败魔法
发布2025-09-25 09:38:15
发布2025-09-25 09:38:15
2150
举报

前言

日常开发中经常需要处理多线程问题。在一次项目优化中,我遇到了一个非常隐蔽的bug,涉及ReentrantLockCondition的使用不当,最终导致程序死锁。这个问题虽然不是特别复杂,但在实际开发中却容易被忽视。本文将详细记录这个bug的出现过程、排查思路以及最终的解决方案,希望能为同行提供一些参考。

问题现象

在项目中有一个任务调度模块,使用了ReentrantLock来控制对共享资源的访问,并通过Condition进行线程等待与唤醒。代码大致如下:

代码语言:java
复制
public class TaskScheduler {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean isRunning = false;

    public void start() {
        lock.lock();
        try {
            if (!isRunning) {
                isRunning = true;
                // 启动任务逻辑
            }
        } finally {
            lock.unlock();
        }
    }

    public void waitUntilRunning() {
        lock.lock();
        try {
            while (!isRunning) {
                condition.await();
            }
        } finally {
            lock.unlock();
        }
    }
}

在测试过程中,发现某些情况下调用waitUntilRunning()方法后程序卡死,无法继续执行。起初我以为是线程阻塞或者条件判断有误,但经过多次调试后才发现问题所在。

问题分析

初步怀疑是condition.await()没有被正确唤醒,导致线程一直等待。为了验证这一点,我在start()方法中添加了日志输出,发现isRunning确实被设置为true,但waitUntilRunning()依然没有返回。这说明线程并未被唤醒。

进一步分析代码逻辑,我发现condition.await()必须在lock持有的情况下调用,否则会抛出IllegalMonitorStateException。但在这个例子中,await()是在lock的保护下调用的,因此理论上不会有问题。

然而,在多线程环境下,如果多个线程同时调用waitUntilRunning(),它们都会进入等待状态。当start()被调用时,只有一个线程能成功获取锁并修改isRunning为true,其他线程可能仍处于等待状态,而start()并没有触发condition.signal()condition.signalAll(),导致其他线程永远无法被唤醒。

排查步骤

第一步:确认线程状态

使用JConsole或VisualVM监控线程状态,发现多个线程处于WAITING状态,且都位于condition.await()处,表明这些线程确实被挂起。

第二步:检查锁的持有情况

查看锁的获取与释放逻辑,确认所有调用lock.lock()的地方都有对应的lock.unlock(),没有明显的锁未释放的问题。

第三步:检查Condition的唤醒逻辑

start()方法中,虽然isRunning被设置为true,但并没有调用condition.signal()condition.signalAll(),因此其他等待的线程无法被唤醒。

第四步:模拟复现问题

编写一个简单的测试类,模拟多个线程调用waitUntilRunning(),然后调用start(),结果发现部分线程始终无法退出await()方法。

代码语言:java
复制
public class TestTaskScheduler {
    public static void main(String[] args) throws InterruptedException {
        TaskScheduler scheduler = new TaskScheduler();

        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + " waiting...");
            scheduler.waitUntilRunning();
            System.out.println(Thread.currentThread().getName() + " resumed");
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        Thread.sleep(1000);
        scheduler.start();
    }
}

运行结果:

代码语言:txt
复制
Thread-0 waiting...
Thread-1 waiting...
Thread-0 resumed

只有第一个线程被唤醒,第二个线程仍然卡住。

总结

这次经历让我深刻认识到,在使用ReentrantLockCondition时,不仅要确保锁的正确获取与释放,还需要注意signal()signalAll()的调用时机。如果没有正确唤醒等待的线程,就可能导致死锁或程序卡死。

此外,Condition的使用比synchronized更灵活,但也更容易出错。建议在使用时保持良好的习惯,比如在每次调用await()前确保锁已被持有,并在适当的时候调用signal()signalAll()

最后,建议在多线程环境中加入日志输出,以便快速定位问题所在。对于复杂的并发场景,可以考虑使用工具如JConsole或VisualVM辅助分析。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 问题现象
  • 问题分析
  • 排查步骤
    • 第一步:确认线程状态
    • 第二步:检查锁的持有情况
    • 第三步:检查Condition的唤醒逻辑
    • 第四步:模拟复现问题
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档