CAS共享锁机制分析

什么是共享锁?

能被多个线程同时获得的锁,为共享锁。也称之为读锁。与互斥锁(写锁)互斥。

AQS原理概述

其核心是一个volatile关键字修饰的int类型的state变量,以及一个由双向指针组成的链表队列。队列再初始化的时候,会CAS生成一个head空节点,后继被阻塞的节点会添加到这个空节点的后面,并相互建立pre和next指针。
在这里插入图片描述

而state变量在不同的锁中,使用方式也有锁不同,比如

  • ReentrantLock 重入锁中,state变量用于记录锁的重入次数,即state>0表示有锁,state=0表示无锁。
  • ReentrantReadWriteLock读写锁,它把int类型的state看作是一个32位的位图,高16位和低16位分别代表了读锁和写锁,高16位表示所有线程读锁的总次数。低16位表示写锁的重入次数。
  • CountDownLatch门闩,state用于记录初始门闩数量,await方法用于把线程加入队列,countDown方法用于减少门闩,当门闩等于0的时候,释放所有阻塞在队列中的线程。
  • Semaphore用来记录信号数量,当state=0时,即新进入的线程会被阻塞到队列。

AQS再代码中使用了模版方法模式,AQS只负责把线程封装成node节点加入列表进行阻塞以及唤醒的工作,至于ReentrantLockReentrantReadWriteLockCountDownLatchSemaphore这些子类如何实现的具体上锁逻辑,AQS并不关心,它只是一个抽象类,把一些公用逻辑进行提炼。

共享锁逻辑分析(Semaphore角度分析AQS共享逻辑)

共享锁获取

	
	//AQS添加锁入口
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            // 小于0表示获取共享锁失败
            /**
             * 获取共享锁失败原因? 
             * 1. 有写线程在执行
             * 2. 公平锁状态下 队列中含有写节点
             * 3.非公平锁状态下,队列head节点为写节点
             */
            //获取共享锁失败,会再次尝试获取锁,如果失败添加队列并阻塞线程
            doAcquireShared(arg);
    }
	
    private void doAcquireShared(int arg) {
        // 封装当前线程以及类型
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是head节点(获取了锁的节点),此时head节点可能已经完成了锁释放,抢锁
                if (p == head) {
                    // 尝试获取锁
                    int r = tryAcquireShared(arg);
                    // 如果获取锁成功,更新head节点,尝试唤醒队列中的后继节点(因为共享锁是可以多线程同时获取,参考:读写锁)
                    if (r >= 0) {
                        // 将当前获取锁的节点更新头部,然后唤醒后继节点。
                        // 四个线程:A B(有锁) {(head初始化的空节点) -> C -> D }
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //确保pre节点能够通知当前节点,并阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
            	//如果中途发生异常,则取消线程
                cancelAcquire(node);
        }
    }

检查并矫正当前节点的前驱节点,确保当前节点可被通知:

	//确保pre节点能够通知当前节点
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /**
             * 如果前驱节点是 SIGNAL(表示前驱节点的状态是活着的,可以通知你)
             * ,那么是可以安全睡眠的
             */
            return true;
        if (ws > 0) { // 只有取消状态是大于1(但是肯定不是head节点,head节点是已经获得锁的)
            /*
             * 
             */
            do {
                // 拆分代码,即:如果前驱节点是取消状态,则找前驱节点的前驱节点,一直往前找,直到找到活着的节点,
                // 然后相互建立pre以及next指针
                // Node predprev = pred.prev;
                // pred = predprev;
                // node.prev = pred;
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 正常节点,将状态其变为SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        /**
         * 返回 false 之后会在acquireQueued方法中的for循环中继续执行该方法,一直
         * 到tryAcquire方法获得到锁 或者继续走到该方法中的第一个if条件返回true,然后
         * 把当前线程进行阻塞。
         */
        return false;
    }

唤醒阻塞线程

    /**
     * 响应中断的方式阻塞线程
     * 
     * @return
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // this 指明当前线程是阻塞在哪个对象上,后去方便使用jstack命令排查问题
        // 判断是否是中断的方式来唤醒线程的
        // 唤醒线程的两种方式 1.unpark 2. interrupt
        return Thread.interrupted();
    }

更新头节点,并唤醒后继线程:

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        // 更新头节点
        setHead(node);

        if (propagate > 0 ||
                h == null || // 假设C线程在获取锁失败,准备进入队列前尝试获取锁的时候A线程正好释放了锁。head节点还没有初始化
                h.waitStatus < 0 || // 头节点SIGNAL/PROPAGATE状态,表示活跃 可通知 或 传播
                (h = head) == null ||
                h.waitStatus < 0) {
            Node s = node.next;
            // 队列中下一个节点为 读锁节点
            if (s == null || s.isShared())
            	//此方法见目录 加锁、解锁的共用方法。同时解释node节点中PROPAGATE状态的含义
                doReleaseShared();
        }
    }

此方法在获取共享锁逻辑中被调用,共享锁是可以被多个线程同时持有的。

共享锁释放

    public final boolean releaseShared(int arg) {
        // 模版方法,子类实现释放锁逻辑,如果成功,唤醒后继节点
        if (tryReleaseShared(arg)) {
        	//此方法见目录 加锁、解锁的共用方法。同时解释node节点中PROPAGATE状态的含义
            doReleaseShared();
            return true;
        }
        return false;
    }

共用方法

    /**
     * 此方法共享锁获取和释放都会被调用
     */
    private void doReleaseShared() {

        /**
         * 以Semaphor共享锁逻辑分析:
         * 
         * 情况1: 只有一个信号量,A(获得锁) 队列节点 {head -> C -> D}
         * 此时如果A线程调用release释放锁,而队列中有 head -> C ,那么头节点(初始化的空节点)一定
         * 是SIGNAL状态,所以修改头节点状态 SIGNAL -> 0 ,并唤醒后续的C节点,并判断h == head,
         * 也就是判断h是否发生了改变。
         * 
         * 发生改变:
         * 1. C节点获取到信号量,信号量剩余0,再执行到setHeadAndPropagate方法时,会把旧的head节
         * 点(状态为0)保存在线程栈,再更新head节点,在下面if语句判断的时候,旧head条件全部不符
         * 合,因此不会唤醒后续线程,直接进入临界区。
         * 2. 而此时的A线程判断h == head为false,head被更新成了C线程节点,
         * 进而从新循环,假如此时没有阻塞线程了,也就是说h != tail为false,那么就不会更新C节点的状态(也就
         * 不会唤醒后续节点的步骤,此时的C的状态还是SIGNAL),没有后续节点被唤醒,自然head不会被更新,所以
         * h == head为true,跳出循环。
         * 3. 而如果有后续节点,则 h != tail为true,符合条件更新C节点状态为0 然后唤醒后续节点。
         * 
         * 未发生改变:
         * 1. 说明C节点被唤醒后,在执行到setHeadAndPropagate方法时,还未来得及更新head节点,因此A线程在判
         * 断此方法 跳出循环条件 h == head 为true,跳出循环。(C线程从阻塞状态被唤醒),此时的
         * head节点依然是旧head节点(空节点,状态被A线程更改为0),条件不符合进入此方法,因此
         * 直接进入临界区,临界区执行完毕。
         * 2. C线程调用 release方法释放锁进入当前方法时,发现head节点就是C线程并且不是tail节点(有后续节点)
         *  状态还是SIGNAL的情况下,唤醒后续节点。
         * 
         * 情况1总结:
         * head被后续唤醒线程改变,是A线程拿到C线程的node唤醒D节点。
         * 后续节点未来得及更新head,则是C线程执行release发放时,使用C节点的node唤醒D节点
         * 
         * 
         * 情况2: 两个信号量,AB(获得锁),队列节点 {head -> C -> D}
         * 此处重点是node节点的PROPAGATE状态解释。
         * 
         * 1. AB两个线程同时调用release进行锁释放,在进入该方法时,线程栈中获取到的head节点都应该是一个空
         * 节点(初始化的节点),同时判断head节点状态为 SIGNAL 时进行CAS修改head节点状
         * 2. 假设A线程CAS竞争成功修改head节点状态为0,唤醒C线程(C线程调用acquire方法,因为没有信号量被阻塞)。
         * B线程CAShead状态->0失败,就会跳过当前 循环从新循环修改head节点状态为PROPAGATE状态。
         * 3. 此时会出现两种情况:
         * a) 唤醒的C线程没有更新head节点
         * b)唤醒的C线程更新了head节点
         * 4. 第一种情况:唤醒的C线程没有来得及更新head节点。AB线程成判断h == head 为true,AB线程释放完锁结束循环。
         * 被唤醒的C线程执行到setHeadAndPropagate方法时更新自己为head节点,同时发现旧head节点的状态为
         * PROPAGATE(head节点先被A线程修改为了0,然后又被B线程修改为PROPAGATE,即 -3)满足 h.waitStatus < 0
         * 的条件,然后进入当前方法,唤醒了线程D。
         * 5. 第二种情况,唤醒的C线程更新了head节点。 AB线程判断h == head 为false,即发现head被改变,那么AB线程会
         * 从新循环获取head节点(C线程节点),并CAS唤醒所有后继线程节点,一直到信号量为0,head不再更新,所有线程跳出循环。
         * 
         */
        for (;;) {

            Node h = head;
            // h != tail 表明后续还有节点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 当前节点状态为活跃节点
                if (ws == Node.SIGNAL) {
					//CAS修改头节点状态为0,成功则唤醒线程,失败则跳过当前循环
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                        continue; // loop to recheck cases
                    }
                    //唤醒线程
                    unparkSuccessor(h);
                } else if (ws == 0 &&		//ws=0 表示有线程已经唤醒了后继节点,当前线程会跳过唤醒再次循环走到这里
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                    // 如果head没有改变,但是CAS失败,则跳过此次循环,从新刷新head节点
                    continue;
                }
            }
            // 从始至终,head节点未发生改变 ,表明head没有被更新
            if (h == head) // loop if head changed
                break;
        }
    }

以上就是我对AQS共享机制的分析,如果有理解错误的地方,欢迎大家留言指正,一起学习共同进步

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值