AbstractQueuedSynchronizer应用之读写锁

本文详细解析读写锁的工作原理及其实现方法,重点介绍了读写锁在AbstractQueuedSynchronizer框架中的具体实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文的代码部分理解,建议结合视频:https://edu.youkuaiyun.com/course/play/25414/301467

思路分析

首先分析读写锁的需求:
1、读锁状态下,可以继续加读锁,但是不能加写锁;如果有写锁在等待队列,后续请求的读锁也需要加到等待队列中。
2、写锁状态下,不能加其他锁。
3、读锁获得锁之后,需要通知后继节点中(在第一个写锁之前)的读锁。

如何实现呢?
1、第一、二点要在获取锁(tryAcquire)中控制;
2、第三点需要在读锁获得锁之后进行
3、写锁的获取、释放跟普通锁的获取、释放相同;读锁的获取、释放有自己的不同之处,所以实现 acquireShared,releaseShared方法

代码讲解

1、acquireShared

 /**
     * Acquires in shared mode, ignoring interrupts.  Implemented by
     * first invoking at least once {@link #tryAcquireShared},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquireShared} until success.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquireShared} but is otherwise uninterpreted
     *        and can represent anything you like.
     */
    public final void acquireShared(int arg) {
        //用返回值表示是否成功获取锁,<0表示失败;>=0 表示成功,其中 =0表示没有需要释放的后继读锁 >0表示有需要释放的后继读锁
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

  读锁加入等待队列

/**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {//如果是头节点的下一个节点,可能在这个过程中,前面的x锁或读锁释放
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);//设置新head,并且传递释放后续读锁
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 /**
     * Sets head of queue, and checks if successor may be waiting
     * in shared mode, if so propagating if either propagate > 0 or
     * PROPAGATE status was set.
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
    //设置新的头节点,并检查当前节点后是否又有新的读节点加入
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        //如果后面有等待的其他节点(propagate>0)
        // 或者 原来的头节点或新的头节点(即node)为待通知状态
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
                (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())//如果当前节点后面的节点为共享锁节点
                doReleaseShared();//释放后继共享锁的关键方法
        }
    }


2、releaseShared

 /**
     * Releases in shared mode.  Implemented by unblocking one or more
     * threads if {@link #tryReleaseShared} returns true.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryReleaseShared} but is otherwise uninterpreted
     *        and can represent anything you like.
     * @return the value returned from {@link #tryReleaseShared}
     */
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//读锁的获取不需要其他读锁的释放来进行通知,但是写锁的获取需要
            doReleaseShared();
            return true;
        }
        return false;
    }

释放后继读锁的关键方法

    /**
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    //通知后继节点并保证传播
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  
         * This proceeds in the usual  way of trying to unparkSuccessor of head if it needs
         * signal. 
         * But if it does not, status is set to PROPAGATE to ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added  while we are doing this. 
         * Also, unlike other uses of unparkSuccessor, we need to know if CAS to reset status fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {//表示后面有等待节点
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//通过cas将节点等待状态从待通知变为已获得锁,如果失败,表示有其他线程在进行了
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);//unpack后续节点
                }
                else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    //如果有新的进程修改了head节点,会出现waitStatus为0,要将其改为PROPAGATE,来保证传播行为继续
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
            //如果有新的进程修改了head节点,循环继续
        }
    }

这个部分是读写锁最难理解的部分,下面进行解释(此处感谢参考文档:https://segmentfault.com/a/1190000016447307):
假设此时写锁释放,一个读节点(称为node0)刚获得锁,等待队列(所有节点都是读节点)如下图所示:

h == head 的作用:

node0动作: node0 unpark了node1,此时如果 h == head,则表示node1尚未获取锁,并将head改为自己;
如果 h != head,表示node1已经获得锁,并将head改为自己,此时继续循环,可以不等到node1来释放node2,直接自己去unpark node2。
此时队列如下图:


else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))没有作用:

因为PROPAGATE的作用在于保证传播,而实际过程如下(此处propagate=tryAcquireShared方法将返回队列中等待节点的个数):

1)
node0动作:
此时node2尚未设置head,head仍为node1,所以h==head,循环结束。

node1动作: 
在setHeadAndPropagate时发现propagate>0,继续释放后续读锁,若此时node2将head更新为自己,队列如下图所示:

,node1将node2的waitStatus改为0,unpark node3,队列如下图所示:


此时node3尚未设置head,循环结束。

2)

node2动作: 
在setHeadAndPropagate时发现propagate>0,继续释放后续读锁。若此时node3尚未设置head,不执行else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)),循环结束。

node3动作:
设置了新head,并在setHeadAndPropagate时发现propagate>0,继续释放后续读锁。此时队列如下图:

更新 node3的waitStatus为0,且unpark node4。


由以上描述可知释放后续读锁的动作没有中断,并且只有在当前head节点状态为SIGNAL的情况下才会unpark后继节点,将head设置成PROPAGATE并不会因此提早unpark后继节点,所以对释放速度也没有优化。
对node的waitStatus从PROPAGATE修改为SIGNAL只有当队列只有当前一个节点(没有等待节点),且新增申请读锁的情况,此时队列没有被写锁占有,且等待队列为空,所以直接就申请锁成功了。

ReadWriteLock实现

jdk实现的ReentrantReadWriteLock比较复杂,现结合我自己实现的简单的MyReadWriteLock进行讲解:

首先,需要定义一个类MyReadWriteLock,它里面有ReadLock,WriteLock实例,它们的lock、unlock方法,分别通过AbstractQueuedSynchronizer实现子类的tryAcquireShared,tryReleaseShared,tryAcquire,tryRelease实现。
这几个方法分别怎么实现呢?返回去看需求:1、读锁状态下,可以继续加读锁,但是不能加写锁;但是如果有写锁在等待队列,后续的读锁也需要加到队列中。

tryAcquireShared方法

        private final static int WRITE_MASK=1024;
        @Override
        protected int tryAcquireShared(int arg) {
            while(!isHeldByWriteLock()){//如果现在不是写锁状态
                if(!getExclusiveQueuedThreads().isEmpty()){//如果有等待的写锁
                    return -1;
                }

                if(compareAndSetState(getState(),getState()+1)){//如果争抢成功
                    //apparentlyFirstQueuedIsExclusive
                    //如果第一个等待的线程为写锁
                    if(writeLockAtTopOfWaitingQueue()){
                        return 0;
                    }
                    return getSharedQueuedThreads().size();
                }
            }
            return -1;
        }

        private boolean writeLockAtTopOfWaitingQueue() {
            return !getExclusiveQueuedThreads().isEmpty() && getFirstQueuedThread()==getExclusiveQueuedThreads().iterator().next();
        }

        protected boolean isHeldByWriteLock() {
            return getState()/WRITE_MASK>0;
        }

其中isHeldByWriteLock是通过state的值来判断是否被写锁占有,这就涉及到tryAcquire的实现:

protected boolean tryAcquire(int arg) {
            //因为加写锁的时候,一定没有读锁占有
            while(getState()==0){
                if(compareAndSetState(0,WRITE_MASK)){
                    return true;
                }
            }
            return false;
        }

最后的两个release方法就比较简单了:

       @Override
        protected boolean tryRelease(int arg) {
            if(getExclusiveOwnerThread()==Thread.currentThread()){
                compareAndSetState(WRITE_MASK,0);
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            if(如果读线程列表中含有当前线程){
                compareAndSetState(getState(),getState()-1);
                return true;
            }
            return false;
        }

在这个基础上加上对锁重入的控制,并且将state的运算优化为位运算,就是java的ReentrantReadWriteLock实现。

本篇文章以abstractQueuedSynchronizer中的方法为基础分析读写锁的需求、实现方法;如果要深入理解相关代码的设计思路,需要结合ReadWriteLock的实际实现类,本文举了一个简单的例子,实际真正使用时需要使用jdk的实现:ReentrantReadWriteLock,以下是一篇介绍:https://blog.youkuaiyun.com/LiuRenyou/article/details/98312234

对于jdk系统类的调试方法

在分析、回放aqs相关执行流程的时候,用脑子有时候难以胜任,需要借助工具,比如泳道图、intellij 的调试功能;在此进行举例:

比如读写锁这一块,笔者怀疑PROPAGATE这个waitStatus值存在的必要性,便将doReleaseShared方法中那一句修改waitStatus的代码注释掉,然后进行调试。调试代码如下:

   public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();

        new Thread(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                writeLock.unlock();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                readLock.lock();
//                latch.countDown();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                readLock.lock();
//                latch.countDown();
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                readLock.lock();
                System.out.println("haha");
            }
        }).start();
    }

但是这里有一个问题:运行时使用的AbstractQueuedSynchronizer是jdk自带的实现,即使你在本intellij idea的模块src目录下作了覆盖,这涉及到类加载机制。一个解决办法是通过endorsed方式:

首先将AbstractQueuedSynchronizer打包成一个jar包,即在cd到src/main/java目录后: 

javac java/util/concurrent/locks/AbstractQueuedSynchronizer.java

jar -cvf myaqs.jar  java/util/concurrent/locks/AbstractQueuedSynchronizer*.class

然后在java的debug configurations的vm arguments处设置:

-Djava.endorsed.dirs=/Users/zhangyugu/IdeaProjects/practice-Code/testCode/src/main/java/

如此运行时使用的便是我们覆盖的AbstractQueuedSynchronizer类。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值