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

问题描述 投票:1回答:4

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

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();
  }
}
java multithreading deadlock reentrantlock
4个回答
2
投票

首先,没有保证线程首先开始。要获得死锁,其中一个线程必须锁定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


1
投票

这确实可能导致死锁,但并非总是如此,例如,如果processThis()完全执行,然后processThat(),反之亦然,没有死锁。您可以尝试添加Thread.delay(100)或Thread.yield()来控制线程执行死锁,甚至将解锁移到某个死锁。


1
投票

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

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
投票

两点:

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