1. 概述

在这个教程中,我们将探讨线程锁定所有可拥有同步器的含义。我们将编写一个简单的程序,使用Lock进行同步,并观察在线程转储中会是什么样子。

2. 锁定的可拥有同步器是什么?

每个线程可能都有一个同步器对象列表。列表中的条目代表线程已获取锁的所有可拥有同步器。

AbstractOwnableSynchronizer类的实例可以作为同步器使用。它最常见的子类之一是Sync类,它是像ReentrantReadWriteLock这样的Lock接口实现的字段。

当我们调用ReentrantReadWriteLock.lock()方法时,代码内部会将此委托给Sync.lock()方法。一旦我们获取了锁,Lock对象就会添加到线程的锁定可拥有同步器列表中。

我们可以在典型的线程转储中查看这个列表:

"Thread-0" #1 prio=5 os_prio=0 tid=0x000002411a452800 nid=0x1c18 waiting on condition [0x00000051a2bff000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at com.baeldung.ownablesynchronizers.Application.main(Application.java:25)

   Locked ownable synchronizers:
        - <0x000000076e185e68> (a java.util.concurrent.locks.ReentrantReadWriteLock$FairSync)

根据我们使用的生成工具,可能需要提供特定选项。例如,使用jstack,我们可以运行以下命令:

jstack -l <pid>

通过-l选项,我们告诉jstack打印有关锁的额外信息。

3. 锁定的可拥有同步器的作用

可拥有同步器列表帮助我们识别可能的应用程序死锁。例如,在线程转储中,我们可以看到名为Thread-1的另一个线程是否正在等待获取同一Lock对象的锁:

"Thread-1" #12 prio=5 os_prio=0 tid=0x00000241374d7000 nid=0x4da4 waiting on condition [0x00000051a42fe000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076e185e68> (a java.util.concurrent.locks.ReentrantReadWriteLock$FairSync)

线程Thread-1处于WAITING状态。具体来说,它等待获取具有ID<0x000000076e185e68>的对象锁。然而,同一对象出现在线程Thread-0的锁定可拥有同步器列表中。现在我们知道,直到线程Thread-0释放其自身的锁,线程Thread-1都无法继续。

如果同时出现相反的情况,即Thread-1已经获取了Thread-0等待的锁,那么我们就创建了一个死锁。

4. 死锁诊断示例

让我们看一些简单的代码,演示上述所有内容。我们将创建一个由两个线程和两个ReentrantLock对象引发死锁的场景:

public class Application {

    public static void main(String[] args) throws Exception {
        ReentrantLock firstLock = new ReentrantLock(true);
        ReentrantLock secondLock = new ReentrantLock(true);
        Thread first = createThread("Thread-0", firstLock, secondLock);
        Thread second = createThread("Thread-1", secondLock, firstLock);

        first.start();
        second.start();

        first.join();
        second.join();
    }
}

main()方法创建两个ReentrantLock对象。线程Thread-0主要使用firstLock,次要使用secondLock

对于Thread-1,我们将做同样的事情。具体来说,我们的目标是通过让每个线程获取其主锁,然后在尝试获取其次级锁时挂起,从而产生死锁。

createThread()方法为每个线程生成相应的锁:

public static Thread createThread(String threadName, ReentrantLock primaryLock, ReentrantLock secondaryLock) {
    return new Thread(() -> {
        primaryLock.lock();

        synchronized (Application.class) {
            Application.class.notify();
            if (!secondaryLock.isLocked()) {
                Application.class.wait();
            }
        }

        secondaryLock.lock();

        System.out.println(threadName + ": Finished");
        primaryLock.unlock();
        secondaryLock.unlock();
    });
}

为了确保每个线程在其尝试获取之前已经锁定其primaryLock,我们在synchronized块内部使用isLocked()等待。

运行这段代码将挂起并永远不会打印完成的控制台输出。如果我们运行jstack,我们会看到以下内容:

"Thread-0" #12 prio=5 os_prio=0 tid=0x0000027e1e31e800 nid=0x7d0 waiting on condition [0x000000a29acfe000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076e182558>

   Locked ownable synchronizers:
        - <0x000000076e182528>


"Thread-1" #13 prio=5 os_prio=0 tid=0x0000027e1e3ba000 nid=0x650 waiting on condition [0x000000a29adfe000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076e182528>

   Locked ownable synchronizers:
        - <0x000000076e182558>

我们可以看到Thread-0被阻塞,等待0x000000076e182558,而Thread-1等待0x000000076e182528。同时,我们可以在它们各自的线程的锁定可拥有同步器中找到这些句柄。基本上,这意味着我们可以看到我们的线程等待哪些锁,以及哪些线程拥有这些锁。这有助于我们调试并发问题,包括死锁。

需要注意的是,如果我们使用ReentrantLock而不是ReentrantReadWriteLock.ReadLock作为同步器,线程转储中不会获得相同的信息。只有ReentrantReadWriteLock.WriteLock才会出现在同步器列表中。

5. 总结

在这篇文章中,我们讨论了线程转储中出现的锁定的可拥有同步器列表的含义,如何使用它来调试并发问题,以及一个示例场景。如往常一样,本文的源代码可在GitHub上找到。