通过一个简单的例子理解死锁

Understanding deadlock with a simple example

我正在努力了解死锁的基础知识,所以我想出了下面的代码。我有两个线程以相反的顺序获取锁,但它们没有死锁。当我 运行 它时,我看到了所有的打印输出。我做错了什么?

public class DeadlockBasics {
  private Lock lockA = new ReentrantLock();
  private Lock lockB = new ReentrantLock();

  public static void main(String[] args) {
    DeadlockBasics dk = new DeadlockBasics();
    dk.execute();
  }

  private void execute() {
    new Thread(this::processThis).start();
    new Thread(this::processThat).start();
  }

  // called by thread 1
  public void processThis() {
    lockA.lock();
    // process resource A
    System.out.println("resource A -Thread1");

    lockB.lock();
    // process resource B
    System.out.println("resource B -Thread1");

    lockA.unlock();
    lockB.unlock();
  }

  // called by thread 2
  public void processThat() {
    lockB.lock();
    // process resource B
    System.out.println("resource B -Thread2");

    lockA.lock();
    // process resource A
    System.out.println("resource A -Thread2");

    lockA.unlock();
    lockB.unlock();
  }
}

首先,不能保证哪个线程先启动。要获得死锁,其中一个线程必须锁定 lockA,然后第二个线程必须锁定 lockB,反之亦然。

public void processThis() {
    lockA.lock();
    // here the control should be switched to another thread
    System.out.println("resource A -Thread1");

    lockB.lock();
    ...

但是可能没有足够的时间在线程之间切换,因为你只有几行代码..太快了。为了模拟一些长时间的工作,在两种方法的第二次锁定之前添加延迟

lockA.lock();
Thread.sleep(200);  // 200 milis

然后第二个线程将能够在第一个线程释放之前锁定lockB

这确实会导致死锁,但并非总是如此,例如,如果 processThis() 已完全执行,然后再执行 processThat(),反之亦然,则不会出现死锁。您可以尝试添加一个 Thread.delay(100) 或一个 Thread.yield() 来引导线程执行到死锁,甚至删除对某个死锁的解锁。

你的代码是死锁的一个很好的例子,因为 ReenttrantLock 是一个互斥锁,其行为与使用同步的隐式监视器锁访问相同。但是你没有看到因为这部分的死锁:

private void execute() {
      new Thread(this::processThis).start();
      new Thread(this::processThat).start();
}

第一个线程创建并启动后,需要一段时间才能创建第二个线程。 JVM 大约需要 50 us 甚至更少来创建一个新线程,这听起来很短,但足以让第一个线程完成,因此不会发生死锁。

我在您的代码中添加了一个 Thread.sleep(); 以便两个线程可以以某种方式并行执行。

package com.company;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockBasics {
    private Lock lockA = new ReentrantLock();
    private Lock lockB = new ReentrantLock();

    public static void main(String[] args) {
        DeadlockBasics dk = new DeadlockBasics();
        dk.execute();
    }

    private void execute() {
        new Thread(this::processThis).start();
        new Thread(this::processThat).start();
    }

    // called by thread 1
    private void processThis() {
        lockA.lock();
        // process resource A
        try {
            Thread.sleep(1000); //Wait for thread 2 to be executed
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread 1 will own lock a");

        lockB.lock();
        // process resource B
        System.out.println("Thread 1 will own lock b");

        lockA.unlock();
        lockB.unlock();

        // Both locks will now released from thread 1
    }

    // called by thread 2
    private void processThat() {
        lockB.lock();
        // process resource B
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread 2 will own lock b");

        lockA.lock();
        // process resource A
        System.out.println("Thread 2 will own lock a");

        lockA.unlock();
        lockB.unlock();

        // Both locks are released by thread 2
    }
}

两点:

  1. 释放锁的顺序与获取锁的顺序相反。也就是说,processThis 应该颠倒移除锁的顺序。对于您的示例,顺序无关紧要。但是,如果 processThis 试图在释放 B 上的锁之前获取 A 上的新锁,则可能会再次发生死锁。更一般地说,您会发现通过考虑锁的范围并避免重叠但非封闭的范围更容易考虑锁。
  2. 为了更好地突出问题,我会在每个线程中获取第一个锁后调用 wait,并让 execute 启动两个线程 然后 在两个线程上调用 notify