1、死锁
死锁是计算机系统中一个常见的问题,它指的是两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。以下是对死锁的详细解析:
一、死锁的产生原因
死锁的产生通常源于多个进程对资源的争夺,具体可以归纳为以下几点:
- 竞争资源:当多个进程竞争同一组资源,并且每个进程都持有至少一个其他进程所需的资源时,就可能发生死锁。
- 进程间推进顺序非法:进程在运行过程中,请求和释放资源的顺序不当,也可能导致死锁。
二、死锁的必要条件
死锁的发生必须具备以下四个必要条件:
- 互斥条件:进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源进程用毕释放。
- 请求和保持条件:进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 不可抢占条件:进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时自己释放。
- 环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
三、死锁的预防与避免
为了预防或避免死锁的发生,可以采取以下策略:
-
预防死锁:
- 破坏互斥条件:通常不太可能,因为很多资源本身就是互斥的。
- 破坏请求和保持条件:一次性申请所有需要的资源,但这样做可能会降低资源利用率。
- 破坏不可抢占条件:允许系统抢占已分配的资源,但这样做可能导致数据丢失或工作中断。
- 破坏环路等待条件:对资源进行排序,并规定每个进程必须按序请求资源。
-
避免死锁:
- 在系统运行过程中,对进程发出的每一个资源申请进行动态检查,并根据检查结果决定是否分配资源。如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
四、死锁的检测与解除
一旦系统发生死锁,需要采取相应措施来解除死锁,常用的方法包括:
- 资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。
- 撤销进程法:强制撤销部分或全部死锁进程,并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
- 进程回退法:让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
五、死锁示例
假设有两个进程P1和P2,以及两个资源R1和R2。进程P1已经锁定了资源R1并请求资源R2,而进程P2已经锁定了资源R2并请求资源R1。此时,两个进程都无法继续执行,因为它们都在等待对方释放资源,从而形成了死锁。
2、线程死锁
在 Java 中,并发编程时如果不小心管理共享资源,就可能导致线程死锁(Deadlock)。线程死锁是多个线程相互等待对方释放资源,而每个线程都持有对方需要的资源,从而导致这些线程都无法继续执行的现象。
线程死锁的条件
线程死锁通常发生在以下四个条件同时满足时:
- 互斥条件(Mutual Exclusion):
- 保持和等待条件(Hold and Wait):
- 非抢占条件(No Preemption):
- 循环等待条件(Circular Wait):
如何避免 Java 中的线程死锁
要避免 Java 中的线程死锁,可以遵循以下策略:
- 避免嵌套锁:尽量避免一个线程在持有锁的同时再去请求另一个锁。
- 锁定顺序一致:如果必须使用多个锁,确保所有线程以相同的顺序获取锁。
- 使用可重入锁(ReentrantLock):通过
ReentrantLock
的tryLock
方法尝试非阻塞地获取锁,如果获取不到锁,则可以选择放弃或者稍后重试。 - 使用锁超时:设置锁的获取超时时间,防止无限期地等待锁。
- 避免大事务:尽量将事务的范围缩小,避免长时间持有锁。
- 使用并发工具类:Java 并发包(
java.util.concurrent
)中提供了许多高级的并发工具类,如ConcurrentHashMap
、CountDownLatch
、Semaphore
等,这些工具类在内部已经对并发进行了优化,减少了死锁的风险。
示例
以下是一个简单的 Java 线程死锁示例:
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Locked lock1");
try {
Thread.sleep(100); // 故意让线程睡眠,增加死锁发生的可能性
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1: Locked lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Locked lock2");
try {
Thread.sleep(100); // 故意让线程睡眠,增加死锁发生的可能性
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2: Locked lock1");
}
}
});
t1.start();
t2.start();
}
}
在这个例子中,t1
线程首先锁定了 lock1
,然后尝试锁定 lock2
;同时,t2
线程首先锁定了 lock2
,然后尝试锁定 lock1
。由于两个线程都在等待对方释放锁,所以它们都无法继续执行,导致死锁。