暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

都听过线程死锁,线程活锁又是什么东西?

小谢backup 2021-09-20
619

1. 概述

虽然多线程能显著提高应用程序的性能,但它也带来了一些问题。在本文中,我们将借助 Java 示例研究两个多线程问题:死锁和活锁。

2. 死锁

2.1. 什么是死锁?

死锁,即两个或多个线程永远等待被另一个线程持有的锁或资源。死锁线程无法继续执行,应用程序也因此停滞或崩溃。

经典的"哲学家就餐问题"很好地展示了多线程环境中的同步问题,并且经常被用作死锁的范例。

2.2. 死锁示例

我们先通过一个简单的 Java 示例来理解死锁。

在这个例子中,我们将创建两个线程,T1和T2。线程T1调用操作 1,线程T2调用操作2。

要完成它们各自的操作,线程T1需要先获取lock1再获取lock2,而线程T2则需要先获取lock2再获取lock1。两个线程都试图以相反的顺序获取锁。

现在,下面是对应的 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();
  }

  // 其它逻辑

}

复制

运行这个死锁示例并注意输出:

一旦我们运行程序,我们可以看到程序发生死锁并且不会退出。日志显示线程T1正在等待由线程T2持有的lock2。类似地,线程T2正在等待由线程T1持有的lock1。

2.3. 避免死锁

死锁是 Java 中常见的并发问题。我们如何避免死锁呢?

首先,我们应该避免一个线程获取多个锁的场景。如果一个线程确实需要多个锁,我们应该确保每个线程以相同的顺序获取锁,以避免锁获取过程中的任何循环依赖

其次,我们还可以使用定时锁,比如 Lock 接口中的 tryLock 方法,确保线程在无法获取锁时不会无限阻塞。

3.活锁

3.1. 什么是活锁

活锁是另一个并发问题,类似于死锁。在活锁中,两个或多个线程不断在交换彼此的状态,而不是像死锁示例中看到的那样无限等待。活锁会导致线程不能执行它们各自的任务。

活锁的一个很好的例子是消息系统,当发生异常时,消息消费者回滚事务并将消息放回队列的头部。然后从队列中重复读取相同的消息,这又会导致另一个异常并重新被放回队列中。消费者永远不会从队列中获取任何其它消息。

3.2. 活锁示例

现在,为了演示活锁,我们将采用之前讨论过的死锁示例。在此示例中,线程T1调用operation1,线程T2调用operation2。但是,我们将稍微更改这些操作的逻辑。

两个线程都需要两个锁来完成它们的工作。每个线程获取它的第一个锁,但发现第二个锁不可用。因此,为了让另一个线程先完成,每个线程释放它的第一个锁并尝试再次获取这两个锁。

让我们用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();
  }

  // 其它逻辑

}

复制

现在,让我们运行这段代码:

正如我们在日志中看到的,两个线程都在重复获取和释放锁。因此,没有任何线程能够完成该操作。

3.3. 避免活锁

为了避免活锁,我们需要弄清楚导致活锁的原因,然后提出相应的解决方案。

例如,如果我们有两个线程重复获取和释放锁,导致活锁,我们可以设计代码,让线程以随机间隔的时间重试获取锁。这将给线程一个公平的机会来获取它们需要的锁。

在我们之前讨论过的消息系统示例中,处理活锁问题的另一种方法是将失败的消息放在单独的队列中以供进一步处理,而不是再次将它们放回到同一个队列中。

文章转载自小谢backup,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论