图文深入解析 JAVA 读写锁,为什么读锁套写锁会死锁,反过来却不会?

这篇博客深入剖析了Java中的读写锁机制,解释了读写锁的“读写互斥,写写互斥,读读共享”特性。文章详细探讨了读锁在获取和释放资源时对原子state的操作,包括判断写锁状态、公平策略、CAS操作等步骤。同时,博客指出读锁套写锁会导致死锁,而反之则不会,揭示了读写锁设计中的一个潜在问题。此外,文章对比了读锁和写锁与重入锁的区别,并给出了读锁释放资源的流程。
摘要由CSDN通过智能技术生成

一、回顾基本的读写锁

我们知道读写锁 #java.util.concurrent.locks.ReentrantReadWriteLock 是一个 “读写互斥,写写互斥,读读共享” 的锁。

读写锁的使用非常简单,那就是:

我们只需要保证读锁和写锁来自同一个 ReentrantReadWriteLock 即可,我们知道基于 AQS 实现的锁都是使用一个 原子state 来进行资源控制,那么读写锁是如何去控制这个 原子state 的呢?

注:对重入锁 ReentrantLock或者 AQS 源码不熟悉的读者往下阅读会有一定的困难,请阅读 图文深入解析Java显式锁底层源码 —— 加解锁是如何实现的


二、读写锁概述

如果阅读过前面文章或者说对普通重入锁 #java.util.concurrent.locks.ReentrantLock 有一定了解的小伙伴应该知道,重入锁的实现,就是

1、使用 CAS 对 原子state 进行操作,再根据操作的结果来进行资源控制,且当获取资源失败后,

2、使用 On Sync Queue 来进行阻塞等待排队,并等待唤醒以便进行再次对 原子state 进行 CAS 操作来尝试获取资源

的这么一个反复循环的过程。

这里有一个好消息,读写锁中写锁的资源获取acquire与释放release,和重入锁及其类似。读锁在流程上也是分类上面说的两步,但是逻辑则出入较大,不过有了前面的基础,看这篇文章应该不会太吃力。读锁和写锁共用同一个 原子stateOn Sync Queue 来进行资源控制,那么接下来我们来看看这是如何实现的吧。

三、读锁中对于 原子state 的操作

由于写锁和重入锁基本上是一样的,所以我们先讲读锁。上面说到,读锁的实现也可拆为两个阶段,我们先说说第一个阶段:读锁中对于 原子state 的操作。

读锁中对 原子state 的操作,也就是 tryAcquireShared 方法,我们结合源码和源码中的文档,得出如下三步:

  • step1:如果写锁持锁,直接获取资源acquire失败(返回 -1),持有写锁的是本线程除外。
  • step2:写锁不持锁,则首先根据 queue policy(公平锁或非公平锁) 判断一下要不要阻塞。不需要阻塞则有其次,通过修改 原子state 来尝试获取资源,成功则要修改一下重入计数
  • step3:上面的都失败了,则进入到fullTryAcquireShared中。
        protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

另外,源码中的文档注释虽然说了不可重入,但实际上是可以重入的,这里简单的说下,如果对写锁(或者说重入锁Reentrantlock)还有印象的小伙伴应该知道,写锁的重入实际上是对 原子state 进行++操作。而读写则是使用一个 HoldCounter 对象,它的功能很简单,就是负责重入的计数。

但有一个特例,那就是读锁套写锁会死锁,这实际上是读写锁设计上的一个 “缺陷” ,疑问先放在这里,后面我们会娓娓道来。


3.1、原子state 操作之 step1 解析:判断写锁是否已经持锁

这部分逻辑极其简单,但是有一个特殊的设计需要特别关注。

            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;

其一,getExclusiveOwnerThread() != currentexclusiveOwnerThrea

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值