前言
死锁是多线程编程中的常见问题,当多个线程互相持有对方需要的资源并无限等待时,就会发生死锁。 本文将深入探讨导致死锁的四大常见原因,并通过代码示例复现问题,最后给出解决方案。
正文
在并发编程中,死锁是指两个或多个线程相互等待对方释放资源,从而导致程序无法继续执行。
Java中的死锁通常由以下四个必要条件引起:
1. 互斥条件 (Mutual Exclusion):
- 资源一次只能被一个线程占用。
- 如果一个资源被一个线程锁定,其他线程必须等待该资源被释放。
2. 持有并等待条件 (Hold and Wait):
- 一个线程已经持有至少一个资源,同时还在等待获取其他资源。
- 线程在等待其他资源的同时不会释放已经持有的资源。
3. 不可剥夺条件 (No Preemption):
- 资源不能被强制剥夺。
- 一个资源只能在持有它的线程主动释放时才能被其他线程获取。
4. 循环等待条件 (Circular Wait):
-
存在一个线程循环等待的闭环。
-
在这个闭环中,每个线程都在等待下一个线程持有的资源。
为了避免死锁,可以采取以下措施:
1. 避免嵌套锁定:
- 尽量减少嵌套锁定资源的情况。
- 尽可能在一个线程中只持有一个锁。
2. 使用超时机制:
- 在获取锁时设置超时,避免无限期等待。
- 可以使用
tryLock
方法来尝试获取锁,并在获取失败时进行相应处理。
3. 锁定顺序:
- 确保所有线程以相同的顺序获取锁。
- 使用全局锁管理器来管理锁的获取顺序。
4. 使用高级并发工具:
- 使用
java.util.concurrent
包中的锁和同步工具,如ReentrantLock
、Semaphore
等。
1. 锁顺序不一致(Lock Ordering Deadlock)
原因分析:
当多个线程以不同的顺序获取同一组锁时,可能形成循环等待。例如,线程1先获取锁A再获取锁B,线程2先获取锁B再获取锁A,最终互相等待。
代码示例:
package com.dereksmart.crawling.lock;
/**
* @Author derek_smart
* @Date 2025/02/17 7:55
* @Description
*/
public class LockOrderDeadlock {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method1 acquired lockA");
synchronized (lockB) {
System.out.println("Method1 acquired lockB");
}
}
}
public void method2() {
synchronized (lockB) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method2 acquired lockB");
synchronized (lockA) {
System.out.println("Method2 acquired lockA");
}
}
}
public static void main(String[] args) {
LockOrderDeadlock deadlock = new LockOrderDeadlock();
new Thread(deadlock::method1).start();
new Thread(deadlock::method2).start();
}
}
复现死锁:
运行此程序,两个线程卡在获取第二个锁的位置,导致死锁。通过jps、jstack 指令就可发现死锁:
通过上面截图可以发现第一个线程 锁住了<0x000000076c6f80b8> 等待<0x000000076c6f80c8> 而第二个线程是锁住了<0x000000076c6f80c8>等待<0x000000076c6f80b8>
解决方案:
统一锁的获取顺序。例如,按锁对象的哈希值排序。
public void fixedMethod(Object a, Object b) {
Object first = System.identityHashCode(a) < System.identityHashCode(b) ? a : b;
Object second = (first == a) ? b : a;
synchronized (first) {
System.out.println(Thread.currentThread().getName() + " acquired lock on " + first);
try {
Thread.sleep(2000); //
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (second) {
System.out.println(Thread.currentThread().getName() + " acquired lock on " + second);
System.out.println(Thread.currentThread().getName() + " is executing business logic.");
}
}
}
public static void main(String[] args) throws InterruptedException {
LockOrderDeadlock deadlock = new LockOrderDeadlock();
/* new Thread(deadlock::method1).start();
new Thread(deadlock::method2).start();*/
Thread t1 = new Thread(() -> deadlock.fixedMethod(deadlock.lockA, deadlock.lockB), "Thread-1");
Thread t2 = new Thread(() -> deadlock.fixedMethod(deadlock.lockB, deadlock.lockA), "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
}
2. 动态锁顺序死锁(Dynamic Lock Order Deadlock)
原因分析:
锁的顺序由外部参数动态决定。例如,转账时根据账户参数顺序获取锁,不同线程传入相反顺序的账户。
代码示例:
package com.dereksmart.crawling.lock;
public class DynamicLockOrderDeadlock {
static class Account {
private int balance;
public void transfer(Account to, int amount) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "get this lock");
try {
Thread.sleep(2000);
synchronized (to) {
System.out.println(Thread.currentThread().getName() + "get to lock");
Thread.sleep(2000);
if (this.balance >= amount) {
this.balance -= amount;
to.balance += amount;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Account a = new Account();
Account b = new Account();
new Thread(() -> a.transfer(b, 100)).start();
new Thread(() -> b.transfer(a, 200)).start(); // 反向调用导致死锁
}
}
解决方案:
将所有需要锁定的对象集中管理,并一次性获取所有必要的锁。这种方法可以避免因不同线程以不同顺序请求相同资源而导致的死锁
package com.dereksmart.crawling.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author derek_smart
* @Date 2025/02/17 7:55
* @Description 对象集中管理
*/
public class DynamicLockOrderDeadlock1 {
static class Account {
private int balance;
private final Lock lock = new ReentrantLock();
public void transfer(Account to, int amount) {
// 使用全局锁管理器来获取锁
LockManager.acquireLocks(this, to);
try {
System.out.println(Thread.currentThread().getName() + " get locks on both accounts");
if (this.balance >= amount) {
this.balance -= amount;
to.balance += amount;
}
} finally {
// 释放锁
LockManager.releaseLocks(this, to);
}
}
}
public static void main(String[] args) {
Account a = new Account();
Account b = new Account();
new Thread(() -> a.transfer(b, 100)).start();
new Thread(() -> b.transfer(a, 200)).start(); // 反向调用不会导致死锁
}
// 全局锁管理器
static class LockManager {
public static void acquireLocks(Account from, Account to) {
while (true) {
boolean fromLock = false;
boolean toLock = false;
try {
fromLock = from.lock.tryLock();
toLock = to.lock.tryLock();
} finally {
if (fromLock && toLock) {
return;
}
if (fromLock) {
from.lock.unlock();
}
if (toLock) {
to.lock.unlock();
}
}
// 避免忙等待
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void releaseLocks(Account from, Account to) {
from.lock.unlock();
to.lock.unlock();
}
}
}
3. 协作对象间的死锁(Cooperation Object Deadlock)
原因分析:
在持有锁时调用外部方法,而外部方法可能获取其他锁,形成嵌套死锁。
代码示例:
package com.dereksmart.crawling.lock;
/**
* @Author derek_smart
* @Date 2025/02/17 7:55
* @Description 协作对象间的死锁
*/
public class CooperationObjectDeadlock {
static class Resource {
private final String name;
public Resource(String name) {
this.name = name;
}
public synchronized void cooperate(Resource other) {
System.out.println(Thread.currentThread().getName() + " locked " + this.name);
try {
Thread.sleep(50); // 模拟工作
other.finish();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void finish() {
System.out.println(Thread.currentThread().getName() + " finished " + this.name);
}
}
public static void main(String[] args) {
Resource resourceA = new Resource("ResourceA");
Resource resourceB = new Resource("ResourceB");
Thread thread1 = new Thread(() -> resourceA.cooperate(resourceB), "Thread1");
Thread thread2 = new Thread(() -> resourceB.cooperate(resourceA), "Thread2");
thread1.start();
thread2.start();
}
}
解决方案:
使用开放调用(避免在同步代码块中调用外部方法)。
package com.dereksmart.crawling.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author derek_smart
* @Date 2025/02/17 7:55
* @Description 协作对象间的死锁解决方案
*/
public class CooperationObjectDeadlockSolution {
static class Resource {
private final String name;
private final Lock lock = new ReentrantLock();
public Resource(String name) {
this.name = name;
}
public void cooperate(Resource other) {
while (true) {
boolean myLock = false;
boolean otherLock = false;
try {
myLock = this.lock.tryLock();
otherLock = other.lock.tryLock();
if (myLock && otherLock) {
System.out.println(Thread.currentThread().getName() + " locked " + this.name + " and " + other.name);
try {
Thread.sleep(50); // 模拟工作
other.finish();
} catch (InterruptedException e) {
e.printStackTrace();
}
return; // 成功获取到锁,退出循环
}
} finally {
if (myLock) {
this.lock.unlock();
}
if (otherLock) {
other.lock.unlock();
}
}
// 避免忙等待
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void finish() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " finished " + this.name);
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Resource resourceA = new Resource("ResourceA");
Resource resourceB = new Resource("ResourceB");
Thread thread1 = new Thread(() -> resourceA.cooperate(resourceB), "Thread1");
Thread thread2 = new Thread(() -> resourceB.cooperate(resourceA), "Thread2");
thread1.start();
thread2.start();
}
}
代码说明:
-
Resource 类:
- 每个
Resource
对象都有一个ReentrantLock
实例,用于锁定资源。 cooperate
方法使用tryLock
方法尝试获取两个资源的锁。如果不能同时获取两个锁,则释放已获取的锁并重试,避免死锁。finish
方法在操作完成后释放锁。
- 每个
-
主方法:
- 创建两个
Resource
对象,并启动两个线程分别进行协作操作。 - 由于使用了
tryLock
方法,两个线程会以非阻塞的方式尝试获取锁,从而避免死锁。
- 创建两个
通过这种方式,可以有效地避免协作对象间的死锁问题。
4. 资源池死锁(Resource Pool Deadlock)
原因分析:
多个线程竞争有限资源,每个线程持有部分资源并等待其他线程释放,形成循环等待。
代码示例:
package com.dereksmart.crawling.lock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
/**
* @Author derek_smart
* @Date 2025/02/17 7:55
* @Description
*/
public class ResourcePoolDeadlock {
private static final int POOL_SIZE = 2;
private final Semaphore semaphore = new Semaphore(POOL_SIZE, true);
private final List<Object> resources = new ArrayList<>();
public ResourcePoolDeadlock() {
for (int i = 0; i < POOL_SIZE; i++) {
resources.add(new Object());
}
}
public Object acquire() throws InterruptedException {
semaphore.acquire();
synchronized (resources) {
Thread.sleep(2000);
return resources.remove(0);
}
}
public void release(Object resource) {
synchronized (resources) {
resources.add(resource);
}
semaphore.release();
}
public static void main(String[] args) {
ResourcePoolDeadlock pool = new ResourcePoolDeadlock();
new Thread(() -> {
try {
Object res1 = pool.acquire();
Object res2 = pool.acquire(); // 等待第二个资源,但池已空
} catch (InterruptedException e) { /* ... */ }
}).start();
}
}
解决方案:
使用超时机制或按顺序分配资源。
public Object acquireWithTimeout(long timeout) throws InterruptedException {
if (!semaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
synchronized (resources) {
return resources.remove(0);
}
}
总结与预防策略
固定锁顺序:统一锁的获取顺序,避免循环等待。
减少锁粒度:使用细粒度锁或非阻塞数据结构(如ConcurrentHashMap)。
开放调用:不在同步块内调用外部方法。
超时机制:通过tryLock或Semaphore设置超时,避免无限等待。
通过合理设计锁的获取顺序和资源管理,可有效避免死锁问题。