Java 线程死锁和活锁-Java快速进阶教程
1. 概述
虽然多线程有助于提高应用程序的性能,但它也带来了一些问题。在本教程中,我们将在 Java 示例的帮助下研究两个这样的问题,死锁和活锁。
2. 死锁
2.1. 什么是死锁?
当两个或多个线程永远等待另一个线程持有的锁或资源时,就会发生死锁。因此,应用程序可能会停止或失败,因为死锁线程无法进行。
经典的哲学家就餐问题很好地演示了多线程环境中的同步问题,并且经常用作死锁的示例。
2.2. 死锁示例
首先,让我们看一个简单的 Java 示例来理解死锁。
在此示例中,我们将创建两个线程,T1和T2。线程T1调用操作1,线程T2调用操作。
要完成其操作,线程T1需要先获取锁 1,然后获取锁 2,而线程T2需要先获取锁2,然后再获取锁 1。因此,基本上,两个线程都尝试以相反的顺序获取锁。
现在,让我们编写DeadlockExample类:
public class DeadlockExample {
private Lock lock1 = new ReentrantLock(true);
private Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
DeadlockExample deadlock = new DeadlockExample();
new Thread(deadlock::operation1, "T1").start();
new Thread(deadlock::operation2, "T2").start();
}
public void operation1() {
lock1.lock();
print("lock1 acquired, waiting to acquire lock2.");
sleep(50);
lock2.lock();
print("lock2 acquired");
print("executing first operation.");
lock2.unlock();
lock1.unlock();
}
public void operation2() {
lock2.lock();
print("lock2 acquired, waiting to acquire lock1.");
sleep(50);
lock1.lock();
print("lock1 acquired");
print("executing second operation.");
lock1.unlock();
lock2.unlock();
}
// helper methods
}
现在让我们运行这个死锁示例并注意输出:
Thread T1: lock1 acquired, waiting to acquire lock2.
Thread T2: lock2 acquired, waiting to acquire lock1.
运行程序后,我们可以看到程序导致死锁并且永远不会退出。日志显示线程T1正在等待由线程T2 持有的 lock2。同样,线程T2正在等待由线程T1 持有的锁 1。
2.3. 避免死锁
死锁是 Java 中常见的并发问题。因此,我们应该设计一个 Java 应用程序来避免任何潜在的死锁情况。
首先,我们应该避免为一个线程获取多个锁的需要。但是,如果一个线程确实需要多个锁,我们应该确保每个线程以相同的顺序获取锁,以避免锁获取中的任何循环依赖。
我们还可以使用定时锁定尝试,例如 Lock 接口中的tryLock方法,以确保线程在无法获取锁时不会无限阻塞。
3. 活锁
3.1. 什么是活锁
活锁是另一个并发问题,类似于死锁。在 livelock 中,两个或多个线程不断在彼此之间传递状态,而不是像我们在死锁示例中看到的那样无限等待。因此,线程无法执行其各自的任务。
livelock 的一个很好的例子是消息传递系统,当发生异常时,消息使用者回滚事务并将消息放回队列的头部。然后从队列中重复读取相同的消息,只是导致另一个异常并被放回队列中。使用者永远不会从队列中选取任何其他消息。
3.2. 活锁示例
现在,为了演示活锁条件,我们将采用我们之前讨论的相同死锁示例。在此示例中,线程T1调用操作1,线程T2调用操作 2。但是,我们将稍微更改这些操作的逻辑。
两个线程都需要两个锁才能完成其工作。每个线程获取其第一个锁,但发现第二个锁不可用。因此,为了让另一个线程首先完成,每个线程释放其第一个锁并尝试再次获取两个锁。
让我们用一个 LivelockExample类来演示活锁:
public class LivelockExample {
private Lock lock1 = new ReentrantLock(true);
private Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
LivelockExample livelock = new LivelockExample();
new Thread(livelock::operation1, "T1").start();
new Thread(livelock::operation2, "T2").start();
}
public void operation1() {
while (true) {
tryLock(lock1, 50);
print("lock1 acquired, trying to acquire lock2.");
sleep(50);
if (tryLock(lock2)) {
print("lock2 acquired.");
} else {
print("cannot acquire lock2, releasing lock1.");
lock1.unlock();
continue;
}
print("executing first operation.");
break;
}
lock2.unlock();
lock1.unlock();
}
public void operation2() {
while (true) {
tryLock(lock2, 50);
print("lock2 acquired, trying to acquire lock1.");
sleep(50);
if (tryLock(lock1)) {
print("lock1 acquired.");
} else {
print("cannot acquire lock1, releasing lock2.");
lock2.unlock();
continue;
}
print("executing second operation.");
break;
}
lock1.unlock();
lock2.unlock();
}
// helper methods
}
现在,让我们运行此示例:
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T2: cannot acquire lock1, releasing lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: cannot acquire lock1, releasing lock2.
..
正如我们在日志中看到的,两个线程都在反复获取和释放锁。因此,所有线程都无法完成该操作。
3.3. 避免活锁
为了避免活锁,我们需要调查导致活锁的条件,然后提出相应的解决方案。
例如,如果我们有两个线程重复获取和释放锁,导致活锁,我们可以设计代码,以便线程以随机间隔重试获取锁。这将使线程有公平的机会获得所需的锁。
在我们前面讨论的消息传递系统示例中解决活动问题的另一种方法是将失败的消息放入单独的队列中进行进一步处理,而不是将它们再次放回同一队列中。