日常开发中经常需要处理多线程问题。在一次项目优化中,我遇到了一个非常隐蔽的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 删除。