ReentrantReadWriteLock死锁问题解析

it2025-06-04  8

1、案情代码分析:

private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true); public static void main(String[] args){ // thread a new Thread(() -> { System.out.println("a第一次获取读锁"); reentrantReadWriteLock.readLock().lock(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { } System.out.println("a尝试再次获取写锁"); reentrantReadWriteLock.writeLock().lock(); System.out.println("a获取到写锁"); }).start(); // thread b new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("b线程尝试获取到读锁"); reentrantReadWriteLock.readLock().lock(); System.out.println("b线程获取到读锁"); try { TimeUnit.SECONDS.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } reentrantReadWriteLock.readLock().unlock(); System.out.println("b线程释放到读锁"); }).start(); // thread c new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("c线程尝试获取到读锁"); reentrantReadWriteLock.readLock().lock(); System.out.println("c线程获取到读锁"); try { TimeUnit.SECONDS.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } reentrantReadWriteLock.readLock().unlock(); System.out.println("c线程释放到读锁"); }).start(); // reentrantReadWriteLock.writeLock().unlock(); // reentrantReadWriteLock.readLock().unlock(); }

2、输出结果

a第一次获取读锁 b线程尝试获取到读锁 c线程尝试获取到读锁 b线程获取到读锁 c线程获取到读锁 a尝试再次获取写锁 b线程释放到读锁 c线程释放到读锁 。。。。程序并没有停止

3、案情分析

线程b和c创建后休眠2s,确保线程a能够获取读锁,随后b和c线程唤醒,获取读锁;随后b和c继续休眠7s,a线程从5s后醒来,尝试获取写锁;b和c从7s的休眠后醒来,可以释放掉读锁,但是程序并没有停止。

4、使用jstack分析应用的栈信息

"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000003735800 nid=0x4620 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE Locked ownable synchronizers: - None "Thread-0" #11 prio=5 os_prio=0 tid=0x000000001a31a000 nid=0x3940 waiting on condition [0x000000001abbe000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000d6269068> (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) at com.feather.WordCountExample.lambda$main$0(WordCountExample.java:25) at com.feather.WordCountExample$$Lambda$1/1555093762.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None

直接去了线程Thread-0的信息,可以看到线程处于writeLock.lock()方法中,一直在等待唤醒。

5、源码分析

首先这一切发生都是在a、b、c线程获取到读锁之后,a再次获取写锁导致死锁发生。所以我们假定目前ReentrantReadWriteLock中只有共享锁,而接着a尝试在有读锁的时候获取写锁,来看写锁的获取过程。

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

写锁会尝试限制性tryAcquire,如果失败的话就会进入队列中,所以先看tryAcquire这个方法

protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }

tryAcquire的方法比较简单,可以看出先判断当前有无线程拿到锁,就是直接判断state是否为0即可,因为前面已经有a、b、c三个线程获取到了资源,所以c不为0;同时因为当前没有写锁被获取过,所以exclusiveCount(c)返回结果应该也是为false,所以在if (w == 0 || current != getExclusiveOwnerThread()),这个条件应该是直接通过并且整个方法返回false。

因为tryAcquire的返回结果为false,所以我们来看acuqireQueued整个方法(addWaiter方法就是创建一个EXCLUSIVE的节点并且入队)

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

因为当前只有a尝试获取写锁,并且因为失败入队,所以当前队列只有一个虚头节点,以及准备获取写锁的线程a(从这里也可以看出尽管是同一个线程,但是写锁和读锁也是分开判断的)。

这里tryAcquire在公平锁和非公平锁有两种不同的实现:

// 非公平锁 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

在非公平锁中,分两种情况:

1)直接尝试CAS,失败则返回false;

2)当前线程已经获取过写锁了,所以直接重入即可。

// 公平锁 protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }

而在公平锁中,先判断是否有其他线程获取过资源,然后再判断是否有线程拿过写锁,很明显这里会在if (w == 0 || current != getExclusiveOwnerThread())进入,并返回结果false。

所以在已有多数线程获取读锁的情况下,其中一个线程重复获取写锁会失败,并在acuqireQueued这个方法中,进入shouldParkFailedAcquire进行判断。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

shouldParkAfterFailedAcquire 这个方法不陌生了,虽然刚开始创建的头节点wai'tStatus=0,但是架不住acuqireQueued外面有一个死循环,所以会在tryAcquire获取失败的时候继续进来,并且返回结果true,并最终进入parkAndCheckInterrupt自行挂起。

问题就是在这,线程a获取写锁导致线程被挂起,同样也挂起了前面a获取的读锁;虽然b、和c释放了读锁,因为a一直没法释放读锁导致a的写锁一直无法迟迟等待a自己释放读锁,最终陷入死锁。下面是读锁尝试释放所的代码

protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; // a读锁没有释放,所以nextc仍然不等于0 if (compareAndSetState(c, nextc)) return nextc == 0; } }

6、总结

    不要在获取了读锁的情况下在获取写锁,但是在拿到写锁的时候可以拿读锁(锁降级)。

最新回复(0)