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上找到。