这是使用 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)
这看起来像是一个僵局,但有几件事让我怀疑:
- 我找不到可能持有相同锁的另一个线程
- 4 秒后进行线程转储会产生相同的结果,但所有线程现在都报告
parking to wait for <0x00000002a7daa878>
,这与第一次转储中的 0x00000002b8dd4ea8
不同。
这是死锁吗?我看到线程状态有 some 变化,但它只能在锁实现内部发生。还有什么可能导致这种行为?
Is this a deadlock?
我认为这不是 死锁 的证据。至少,不是经典意义上的术语。
堆栈转储显示两个线程在同一 ReentrantReadWriteLock
上等待。一个线程正在等待获取读锁。另一个正在等待获取写锁。
现在,如果当前没有线程持有任何锁,那么这些线程之一将能够继续。
如果其他线程当前持有写锁,则足以阻塞这两个线程。但这不是僵局。如果第三个线程 本身 正在等待不同的锁......并且在阻塞中存在循环,那只会是一个死锁。
那么这两个线程互相阻塞的可能性如何呢?我不认为这是可能的。 javadocs中的重入规则允许拥有写锁的线程获取读锁而不阻塞。同样,它可以获取它已经持有的写锁。
另一个证据是您稍后获取的线程转储中的内容发生了变化。如果真的出现僵局,就不会有任何改变。
如果这不是(只是)这两个线程之间的死锁,那还会是什么?
一种可能是第三个线程持有写锁(很长时间),这会把事情搞砸。对此读写锁的争用过多。
如果(假定的)第三个线程正在使用 tryLock
,您可能有一个 活锁 ... 这可以解释“更改”证据。但另一方面,该线程也应该被停放......你说你没有看到。
另一种可能性是您有太多活动线程...并且 OS 正在努力将它们安排到核心。
但这都是猜测。
原来是死锁。持有锁的线程在线程转储中未报告为持有任何锁,这使得诊断变得困难。
了解这一点的唯一方法是检查应用程序的堆转储。对于那些对如何操作感兴趣的人,这里是一步一步的过程:
- 堆转储与线程转储大致同时进行。
- 我使用 Java VisualVM 打开它,它带有 JDK。
- 在“类”视图中,我按 class 的 class 名称进行过滤,其中包含作为字段的锁。
- 我双击 class 进入“实例”视图
- 谢天谢地,这种情况只有少数几个 class,所以我能够找到导致问题的那个。
- 我检查了保存在 class 字段中的
ReentrantReadWriteLock
对象。特别是该锁的 sync
字段保持其状态 - 在这种情况下它是 ReentrantReadWriteLock$FairSync
.
- 它的
state
属性 是 65536
。这表示锁的共享和独占持有数。共享持有计数存储在状态的前 16 位中,并检索为 state >>> 16
。独占保留计数在最后 16 位中,检索为 state & ((1 << 16) - 1)
。从这里我们可以看到锁上有 1 个共享持有和 0 个独占持有。
- 您可以在
head
字段中看到等待锁的线程。它是一个队列,thread
包含等待线程,next
包含队列中的下一个节点。通过它,我找到了 writer-0
和 8 个 reader-n
线程中的 7 个,证实了我们从线程转储中知道的内容。
- 同步对象的
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 线程无法正确释放锁并永远保留它。这是我通过分析代码发现的,并在获取和释放锁时添加跟踪和日志记录。
我有一个应用程序有 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)
这看起来像是一个僵局,但有几件事让我怀疑:
- 我找不到可能持有相同锁的另一个线程
- 4 秒后进行线程转储会产生相同的结果,但所有线程现在都报告
parking to wait for <0x00000002a7daa878>
,这与第一次转储中的0x00000002b8dd4ea8
不同。
这是死锁吗?我看到线程状态有 some 变化,但它只能在锁实现内部发生。还有什么可能导致这种行为?
Is this a deadlock?
我认为这不是 死锁 的证据。至少,不是经典意义上的术语。
堆栈转储显示两个线程在同一 ReentrantReadWriteLock
上等待。一个线程正在等待获取读锁。另一个正在等待获取写锁。
现在,如果当前没有线程持有任何锁,那么这些线程之一将能够继续。
如果其他线程当前持有写锁,则足以阻塞这两个线程。但这不是僵局。如果第三个线程 本身 正在等待不同的锁......并且在阻塞中存在循环,那只会是一个死锁。
那么这两个线程互相阻塞的可能性如何呢?我不认为这是可能的。 javadocs中的重入规则允许拥有写锁的线程获取读锁而不阻塞。同样,它可以获取它已经持有的写锁。
另一个证据是您稍后获取的线程转储中的内容发生了变化。如果真的出现僵局,就不会有任何改变。
如果这不是(只是)这两个线程之间的死锁,那还会是什么?
一种可能是第三个线程持有写锁(很长时间),这会把事情搞砸。对此读写锁的争用过多。
如果(假定的)第三个线程正在使用 tryLock
,您可能有一个 活锁 ... 这可以解释“更改”证据。但另一方面,该线程也应该被停放......你说你没有看到。
另一种可能性是您有太多活动线程...并且 OS 正在努力将它们安排到核心。
但这都是猜测。
原来是死锁。持有锁的线程在线程转储中未报告为持有任何锁,这使得诊断变得困难。
了解这一点的唯一方法是检查应用程序的堆转储。对于那些对如何操作感兴趣的人,这里是一步一步的过程:
- 堆转储与线程转储大致同时进行。
- 我使用 Java VisualVM 打开它,它带有 JDK。
- 在“类”视图中,我按 class 的 class 名称进行过滤,其中包含作为字段的锁。
- 我双击 class 进入“实例”视图
- 谢天谢地,这种情况只有少数几个 class,所以我能够找到导致问题的那个。
- 我检查了保存在 class 字段中的
ReentrantReadWriteLock
对象。特别是该锁的sync
字段保持其状态 - 在这种情况下它是ReentrantReadWriteLock$FairSync
. - 它的
state
属性 是65536
。这表示锁的共享和独占持有数。共享持有计数存储在状态的前 16 位中,并检索为state >>> 16
。独占保留计数在最后 16 位中,检索为state & ((1 << 16) - 1)
。从这里我们可以看到锁上有 1 个共享持有和 0 个独占持有。 - 您可以在
head
字段中看到等待锁的线程。它是一个队列,thread
包含等待线程,next
包含队列中的下一个节点。通过它,我找到了writer-0
和 8 个reader-n
线程中的 7 个,证实了我们从线程转储中知道的内容。 - 同步对象的
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 线程无法正确释放锁并永远保留它。这是我通过分析代码发现的,并在获取和释放锁时添加跟踪和日志记录。