破坏死锁的四个条件
银行家算法
按顺序请求资源
尽量避免嵌套锁
尝试锁定trylock
避免死锁 是并发编程中的一个重要问题。死锁是指多个线程在等待彼此持有的资源,导致无法继续执行的状态。在 Java 中,死锁通常发生在多线程程序中,尤其是在使用同步块、锁和其他并发机制时。
避免死锁有几种常见的策略和技术,下面详细介绍:
1. 资源申请顺序一致(避免循环等待)
问题: 死锁的一个重要条件是线程在不同的顺序上请求资源,导致循环等待的情况发生。
解决方法: 通过规定资源获取的顺序,使所有线程都按照相同的顺序请求锁或资源,避免产生循环等待。
示例:
class Resource1 {}
class Resource2 {}
public class DeadlockAvoidance {
private final Resource1 r1 = new Resource1();
private final Resource2 r2 = new Resource2();
public void method1() {
synchronized (r1) {
synchronized (r2) {
// 对资源 r1 和 r2 的操作
System.out.println("Method 1");
}
}
}
public void method2() {
synchronized (r1) { // 保持与 method1 相同的锁获取顺序
synchronized (r2) {
// 对资源 r1 和 r2 的操作
System.out.println("Method 2");
}
}
}
public static void main(String[] args) {
DeadlockAvoidance da = new DeadlockAvoidance();
new Thread(da::method1).start();
new Thread(da::method2).start();
}
}
解释: 在 method1()
和 method2()
中,两个线程获取资源的顺序是相同的(先 r1
,再 r2
)。如果每个线程都按照相同的顺序获取资源,循环等待就不会发生,从而避免了死锁。
2. 尝试锁定(使用 tryLock
)
问题: 当线程获取多个锁时,如果某个锁被占用,线程会一直等待,可能造成死锁。
解决方法: 使用 java.util.concurrent.locks.Lock
接口提供的 tryLock()
方法,它允许线程尝试获取锁。如果锁不可用,线程可以选择不等待,而是执行其他逻辑,避免死锁。
示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void tryLockMethod1() {
try {
if (lock1.tryLock() && lock2.tryLock()) {
try {
// 对资源的操作
System.out.println("Method 1 acquired both locks");
} finally {
lock2.unlock();
lock1.unlock();
}
} else {
System.out.println("Method 1 failed to acquire locks");
}
} finally {
// 释放锁的操作
}
}
public void tryLockMethod2() {
try {
if (lock2.tryLock() && lock1.tryLock()) {
try {
// 对资源的操作
System.out.println("Method 2 acquired both locks");
} finally {
lock1.unlock();
lock2.unlock();
}
} else {
System.out.println("Method 2 failed to acquire locks");
}
} finally {
// 释放锁的操作
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
new Thread(example::tryLockMethod1).start();
new Thread(example::tryLockMethod2).start();
}
}
解释: 通过 tryLock()
方法,线程尝试获取锁。如果锁不可用,它可以立即返回 false
,然后执行其他操作而不是一直等待,从而避免了死锁的发生。
3. 超时获取锁
问题: 在某些情况下,线程可能永远无法获取到锁,导致死锁。
解决方法: 使用带有超时的 tryLock(long timeout, TimeUnit unit)
方法来防止无限期等待锁。线程尝试在给定的时间内获取锁,如果超时,它将放弃对锁的请求,从而减少死锁的可能性。
示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class LockTimeoutExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
try {
if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
// 对资源的操作
System.out.println("Method 1 acquired both locks");
} finally {
lock2.unlock();
}
} else {
System.out.println("Method 1 failed to acquire lock2");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("Method 1 failed to acquire lock1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2() {
try {
if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
// 对资源的操作
System.out.println("Method 2 acquired both locks");
} finally {
lock1.unlock();
}
} else {
System.out.println("Method 2 failed to acquire lock1");
}
} finally {
lock2.unlock();
}
} else {
System.out.println("Method 2 failed to acquire lock2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
LockTimeoutExample example = new LockTimeoutExample();
new Thread(example::method1).start();
new Thread(example::method2).start();
}
}
解释: 如果线程无法在指定的超时时间内获取到锁,它将放弃锁的请求,避免死锁的产生。
4. 减少锁的使用范围
问题: 死锁通常发生在锁持有的时间过长或锁的范围过大时。
解决方法: 只在真正需要保护共享资源时获取锁,尽量减少锁的使用范围(即缩小同步块的范围),从而减少产生死锁的概率。
示例:
public class NarrowLockScope {
private final Object lock = new Object();
public void doSomething() {
// 在非关键代码区不使用锁
System.out.println("Doing something outside lock");
synchronized (lock) {
// 仅在真正需要同步时使用锁
System.out.println("Doing something with lock");
}
}
}
解释: 锁只用于保护共享资源的关键部分,减少了持有锁的时间,降低了发生死锁的可能性。
5. 避免嵌套锁
问题: 嵌套锁的使用容易导致死锁。即一个线程在持有锁 A 的同时,试图去获取锁 B,另一线程在持有锁 B 时试图获取锁 A,这可能导致死锁。
解决方法: 避免嵌套锁,尽量保持获取单个锁的原则。
示例:
public class SingleLockExample {
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// 执行需要同步的操作
System.out.println("Doing something");
}
}
}
解释: 使用一个锁,避免多个线程同时竞争多个锁,避免了嵌套锁带来的死锁问题。
6. 使用高级并发工具
问题: 手动管理锁的使用容易导致编程复杂性增加,且容易引发死锁问题。
解决方法: 使用 Java 提供的高级并发工具类,如 java.util.concurrent
包中的 Semaphore
、CountDownLatch
、CyclicBarrier
和 ConcurrentHashMap
等来避免死锁。这些工具类通常经过良好的设计和优化,可以帮助简化并发代码,降低死锁风险。
示例:
Semaphore semaphore = new Semaphore(1);
public void doSomething() {
try {
semaphore.acquire(); // 获取信号量
// 执行需要同步的操作
System.out.println("Doing something with semaphore");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放信号量
}
}
解释: 使用 Semaphore
可以有效管理资源的并发访问,避免传统锁机制引发的死锁问题。
总结
为了避免死锁,可以采取以下策略:
- 确保资源获取的顺序一致,避免循环等待。
- 使用
tryLock()
或设置