
日常开发中经常需要处理多线程问题。在一次项目优化中,我遇到了一个非常隐蔽的bug,涉及ReentrantLock和Condition的使用不当,最终导致程序死锁。这个问题虽然不是特别复杂,但在实际开发中却容易被忽视。本文将详细记录这个bug的出现过程、排查思路以及最终的解决方案,希望能为同行提供一些参考。
在项目中有一个任务调度模块,使用了ReentrantLock来控制对共享资源的访问,并通过Condition进行线程等待与唤醒。代码大致如下:
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(),没有明显的锁未释放的问题。
在start()方法中,虽然isRunning被设置为true,但并没有调用condition.signal()或condition.signalAll(),因此其他等待的线程无法被唤醒。
编写一个简单的测试类,模拟多个线程调用waitUntilRunning(),然后调用start(),结果发现部分线程始终无法退出await()方法。
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();
}
}运行结果:
Thread-0 waiting...
Thread-1 waiting...
Thread-0 resumed只有第一个线程被唤醒,第二个线程仍然卡住。
这次经历让我深刻认识到,在使用ReentrantLock和Condition时,不仅要确保锁的正确获取与释放,还需要注意signal()或signalAll()的调用时机。如果没有正确唤醒等待的线程,就可能导致死锁或程序卡死。
此外,Condition的使用比synchronized更灵活,但也更容易出错。建议在使用时保持良好的习惯,比如在每次调用await()前确保锁已被持有,并在适当的时候调用signal()或signalAll()。
最后,建议在多线程环境中加入日志输出,以便快速定位问题所在。对于复杂的并发场景,可以考虑使用工具如JConsole或VisualVM辅助分析。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。