线程同步中的死锁问题
在多线程编程中,多个线程常常需要访问共享资源。为了防止数据混乱,程序员会使用锁机制来实现线程同步。但当锁的使用不当,就可能引发一个棘手的问题——死锁。
死锁指的是两个或多个线程相互等待对方释放锁,导致所有线程都无法继续执行的状态。就像两个人在狭窄的走廊里迎面走来,谁也不肯让路,结果谁也过不去。
死锁的四个必要条件
要发生死锁,必须同时满足以下四个条件:
互斥条件:某个资源一次只能被一个线程占用。
占有并等待:线程已经持有了至少一个资源,还在等待获取其他被占用的资源。
不可抢占:已分配给线程的资源不能被其他线程强行拿走,只能由该线程主动释放。
循环等待:存在一个线程的等待环路,比如线程A等线程B,线程B等线程C,线程C又等线程A。
一个典型的死锁示例
假设两个线程分别尝试按不同顺序获取两把锁:
Thread 1:
synchronized(lockA) {
sleep(100);
synchronized(lockB) {
// 执行操作
}
}
Thread 2:
synchronized(lockB) {
sleep(100);
synchronized(lockA) {
// 执行操作
}
}这种情况下,线程1先拿到lockA,线程2先拿到lockB,接着它们都试图获取对方持有的锁。由于都在等待,最终陷入僵局。
如何避免死锁
最简单的方法是保证所有线程以相同的顺序获取锁。比如都先申请lockA,再申请lockB,这样就不会形成循环等待。
另一个办法是使用带超时的锁请求。Java中的java.util.concurrent.locks.ReentrantLock支持tryLock(long timeout, TimeUnit unit)方法,如果在指定时间内拿不到锁,线程可以选择放弃,转而处理其他任务,而不是无限等待。
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
// 线程尝试获取锁
boolean acquiredA = lockA.tryLock(1, TimeUnit.SECONDS);
if (acquiredA) {
try {
boolean acquiredB = lockB.tryLock(1, TimeUnit.SECONDS);
if (acquiredB) {
try {
// 安全执行临界区代码
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}此外,尽量减少锁的持有时间,只在真正需要时才加锁,也能降低死锁发生的概率。
实际开发中,数据库系统也会遇到类似情况。比如两个事务互相等待对方释放行锁,数据库通常会通过死锁检测机制自动中断其中一个事务,避免系统卡死。
编写多线程程序时,不能只关注功能正确性,还得留意资源调度的合理性。一个看似无害的同步逻辑,可能在高并发下暴露出隐藏的死锁风险。