ReentrantReadWriteLock死锁问题解析

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、案情分析

  1. 线程b和c创建后休眠2s,确保线程a能够获取读锁,随后b和c线程唤醒,获取读锁;
  2. 随后b和c继续休眠7s,a线程从5s后醒来,尝试获取写锁;
  3. 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、总结

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值