这是使用 ReentrantReadWriteLock 时的死锁吗?

Is this a deadlock when using ReentrantReadWriteLock?

我有一个应用程序有 1 个写入线程和 8 个 reader 线程访问共享资源,它位于 ReentrantReadWriteLock 之后。它冻结了大约一个小时,没有产生任何日志输出,也没有响应请求。这是在 Java 8.

在杀死它之前,有人获取了线程转储,如下所示:

作者线程:

"writer-0" #83 prio=5 os_prio=0 tid=0x00007f899c166800 nid=0x2b1f waiting on condition [0x00007f898d3ba000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000002b8dd4ea8> (a java.util.concurrent.locks.ReentrantReadWriteLock$FairSync)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    at java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.lock(ReentrantReadWriteLock.java:943)

Reader:

"reader-1" #249 daemon prio=5 os_prio=0 tid=0x00007f895000c000 nid=0x33d6 waiting on condition [0x00007f898edcf000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000002b8dd4ea8> (a java.util.concurrent.locks.ReentrantReadWriteLock$FairSync)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireShared(AbstractQueuedSynchronizer.java:967)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireShared(AbstractQueuedSynchronizer.java:1283)
    at java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock.lock(ReentrantReadWriteLock.java:727)

这看起来像是一个僵局,但有几件事让我怀疑:

这是死锁吗?我看到线程状态有 some 变化,但它只能在锁实现内部发生。还有什么可能导致这种行为?

Is this a deadlock?

我认为这不是 死锁 的证据。至少,不是经典意义上的术语。

堆栈转储显示两个线程在同一 ReentrantReadWriteLock 上等待。一个线程正在等待获取读锁。另一个正在等待获取写锁。

现在,如果当前没有线程持有任何锁,那么这些线程之一将能够继续。

如果其他线程当前持有写锁,则足以阻塞这两个线程。但这不是僵局。如果第三个线程 本身 正在等待不同的锁......并且在阻塞中存在循环,那只会是一个死锁。

那么这两个线程互相阻塞的可能性如何呢?我不认为这是可能的。 javadocs中的重入规则允许拥有写锁的线程获取读锁而不阻塞。同样,它可以获取它已经持有的写锁。

另一个证据是您稍后获取的线程转储中的内容发生了变化。如果真的出现僵局,就不会有任何改变。


如果这不是(只是)这两个线程之间的死锁,那还会是什么?

一种可能是第三个线程持有写锁(很长时间),这会把事情搞砸。对此读写锁的争用过多。

如果(假定的)第三个线程正在使用 tryLock,您可能有一个 活锁 ... 这可以解释“更改”证据。但另一方面,该线程也应该被停放......你说你没有看到。

另一种可能性是您有太多活动线程...并且 OS 正在努力将它们安排到核心。

但这都是猜测。

原来是死锁。持有锁的线程在线程转储中未报告为持有任何锁,这使得诊断变得困难。

了解这一点的唯一方法是检查应用程序的堆转储。对于那些对如何操作感兴趣的人,这里是一步一步的过程:

  1. 堆转储与线程转储大致同时进行。
  2. 我使用 Java VisualVM 打开它,它带有 JDK。
  3. 在“类”视图中,我按 class 的 class 名称进行过滤,其中包含作为字段的锁。
  4. 我双击 class 进入“实例”视图
  5. 谢天谢地,这种情况只有少数几个 class,所以我能够找到导致问题的那个。
  6. 我检查了保存在 class 字段中的 ReentrantReadWriteLock 对象。特别是该锁的 sync 字段保持其状态 - 在这种情况下它是 ReentrantReadWriteLock$FairSync.
  7. 它的 state 属性 是 65536。这表示锁的共享和独占持有数。共享持有计数存储在状态的前 16 位中,并检索为 state >>> 16。独占保留计数在最后 16 位中,检索为 state & ((1 << 16) - 1)。从这里我们可以看到锁上有 1 个共享持有和 0 个独占持有。
  8. 您可以在head 字段中看到等待锁的线程。它是一个队列,thread 包含等待线程,next 包含队列中的下一个节点。通过它,我找到了 writer-0 和 8 个 reader-n 线程中的 7 个,证实了我们从线程转储中知道的内容。
  9. 同步对象的firstReader字段包含已经获取读取日志的线程-来自代码中的注释firstReader is the first thread to have acquired the read lock. firstReaderHoldCount is firstReader's hold count.More precisely, firstReader is the unique thread that last changed the shared count from 0 to 1, and has not released theread lock since then; null if there is no such thread.

在这种情况下,持有锁的线程是 reader 线程之一。它在完全不同的东西上被阻塞,这将需要其他 reader 个线程之一才能进行。最终它是由一个错误引起的,其中 reader 线程无法正确释放锁并永远保留它。这是我通过分析代码发现的,并在获取和释放锁时添加跟踪和日志记录。