AQS基本原理(代码注释,超详细)

基本思路

通过cas去获取主动权,失败就去排队.持有锁的解锁后唤醒下一个, 被唤醒后的线程看看自己是不是排到了第一.

值得注意的是,排队时候是可以取消的.从队列中删除某个节点,不能出现死循环,指向错误的问题.

由于支持节点的取消:

  • next是不可靠的, 只能使用prev去遍历
  • 遍历可能是不完全的,只靠release的时候去唤醒是不够的,取消的时候也要支持唤醒

本文将描述三个部分: cas, 排队, 唤醒.

基本上就是解释下面这个代码:

    public final void acquire(int arg) {
        //tryAcquire 是一个cas操作 不成功就入队
        //addWaiter 入队 需要注意多个线程同时入队的操作
        //acquireQueued 判断自己是不是第一,不是的话继续park
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //如果中途被发送了中断信号,那就中断自己
            selfInterrupt();
    }

cas

cas全称为compareAndSet. 比较和设值是一个原子操作, 例如cas(v, 1, 2), v是我要设值的变量, 1 为期望值, 2 为目标值. 只有v在等于1的时候能够被设置为2且返回true,其余时候都返回false.

请务必保证v被volatile修饰,各线程才能正常竞争.

tryAcquire

有了cas,获取锁的操作就很简单.写了一个无法排队和阻塞的锁.

public class MyLock {
    /**
     * 记录状态,每lock一次,就+1,反之-1
     */
    private AtomicInteger state = new AtomicInteger(0);
    /**
     * 记录谁持有这个锁
     */
    private Thread current = null;

    /**
    * 尝试获取,非阻塞
    */
    public boolean tryLock() {
        if (current == Thread.currentThread()) {
            //本线程已经持有该锁,重入+1
            state.addAndGet(1);
            return true;
        } else if (state.compareAndSet(0, 1)) {
            //依靠cas最多只让一个线程到达这里
            //当然可以多判断一下state目前是不是等于0再cas
            current = Thread.currentThread();
            return true;
        }
        return false;
    }

    public void unlock() {
        //没有持有该锁的线程进行unlock抛错
        if (current != Thread.currentThread()) {
            throw new IllegalMonitorStateException();
        }
        if (state.get() == 1) {
            //提前解绑 再把state置为0
            current = null;
        }
        //在java里面,只能通知一个线程中断,不存在抢占式中断.所以不用担心这里不能设置为0
        state.addAndGet(-1);
    }
}

但aqs的重点并不是cas,是排队,cas是一种同步的手段,只让一个线程操作成功.

在ReentrantLock的实现中,有公平锁和非公平锁两种实现,顾名思义,公平锁不允许插队.已经有人在排队了,则只能添加到队列末位. 而非公平锁则允许插队.

非公平锁存在饿死的情况,老是被插队,队列中的线程得不到执行的机会.但是和公平锁相比,在刚好锁可用的情况下,减少了一次线程的入队阻塞和一次线程的唤醒.性能会更好.

ReentrantLock中的state是一个计数, 只要state>0,肯定有一个独占线程.state==0,独占线程为null.

    //公平
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //如果没人排队,并且cas成功才能获取到锁
            //是否有人排队的判断就算不够及时也没有关系,本线程进入排队后会立刻查看自己是否在第一位,如果是的话,就会获取到锁
            if (!hasQueuedPredecessors() &&
            //很多线程可以同时执行到这里.大家都认为没人排队
            //需要cas同步,只让一个人通过,否则会有多个线程同时获取锁
            //这个cas的时候,可能有人在排队了,也可能没人在排队但有人获取了锁,不过state都会是大于0
            //也有可能在短时间内,有人获取了锁又释放了,产生了aba问题,这时候是能成功cas的,aba在这个场景没有副作用
                    compareAndSetState(0, acquires)) {
                //设置独占线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            //如果已经持有 重入
            int nextc = c + acquires;
            //达到了最大值,溢出
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    //非公平
    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;
    }

排队

排队的第一步是生成一个Node入队.aqs维护head和tail,队列为一个双向链表.

每个node需要对应一个thread,也需要记录对应的状态.

状态有

  • cancelled = 1 表示被取消 被取消的node需要被移除队列
    • 移出队列有几种方法,一种是被别人跳过,一种是取消时自我移除. 自我移除的同时,如果前面节点同时在取消,那么会失败
  • signal = -1 表示需要唤醒下一个node,换句话说:后面的node在这里打个标记,告知别人这个node后面有需要唤醒的node
  • condition = -2 表示在wait condition
    private Node addWaiter(Node mode) {
        //acquire的mode是独占
        Node node = new Node(mode);

        for (;;) {
            //当前的tail
            Node oldTail = tail;
            if (oldTail != null) {
                //prev指向旧tail,这时候tail可能是发生变化的
                node.setPrevRelaxed(oldTail);
                //将tail通过cas设置成当前node 只有tail不变的情况下,才能设置成功
                //如果失败,有两种情况
                //一种是别的线程要入队比本线程更早成功
                //一种是tail节点被取消,而被移除队列
                //当然也可以存在一个node成为新tail然后又取消出队的情况,aba没有副作用
                if (compareAndSetTail(oldTail, node)) {
                    //成功后,oldTail就是前一个节点了,将next指向新的tail
                    //如果这个时候oldTail被移除队列了,其实没有关系
                    //因为前面可用的node指向了本node.而oldTail的引用将不会有人再持有,等待垃圾回收
                    oldTail.next = node;
                    return node;
                }
            } else {
                //队列还不存在,初始化一下
                //初始化也是通过cas(null, new Node())设置head, tail=head
                //也可能设置失败,被别的线程先设置了
                //头结点只是一个空结点
                //无论如何,都需要进入下一个循环去排队
                initializeSyncQueue();
            }
        }
    }

下图简单的表示两个线程想要排队的情形,是cas的直观表述,下图不包括队列为空,有人取消node等情形:

在这里插入图片描述

入队后的node(thread),会在一个循环中,判断自己是不是排到了第一个,不是的话就挂起

    //参数node就是入队时生成的node
    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            //node自旋
            for (;;) {
                //前一个node
                final Node p = node.predecessor();
                //如果前一个node是head并且抢到了主动权
                //按道理我被唤醒只要前一个是head(head是一个空结点),就轮到我执行了
                //公平锁的情况下,我肯定能tryAcquire成功
                //但是非公平锁的情况下,就算我的前一个是head,tryAcquire不一定是会成功的
                //因为非公平锁,别的线程可以不考虑队列是否为空就去tryAcquire,本线程是和别的线程一起竞争
                if (p == head && tryAcquire(arg)) {
                    //如果我获取成功了,那我已经获取了锁了
                    //把本节点设置为head,并且将thread置空,成为空节点
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                //否则判断一下是否需要park
                //虽然我的前面一个node不是head,但是可能前面的节点都是被取消的
                //我只要清理掉取消的node,我的前一个node就可能是head了
                if (shouldParkAfterFailedAcquire(p, node))
                    //native park 等待被唤醒
                    //可能是中断唤醒的,而不是被正常唤醒的
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            //出现问题 取消
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

    //判断是否需要park
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //已经做过清理打过标记了,安心去park
            //但是假设我排在第二,我先判断第一节点是signal的,然后park了,接着持有锁的线程释放锁,第一个节点被unpark
            //恰好这时候第一节点又被取消了,unpark会拿到一个取消结点,那么按照顺序本节点应该要被唤醒
            //所以unpark需要遍历去找到第二个节点,next是不可靠的,只能靠prev去遍历
            //但prev遍历也会存在无法遍历全节点的问题
            //假如我是在prev遍历的时候加入的,遍历时队列只有第一个取消结点,接着加入了第二个可用结点,那么遍历会认为队列是空了
            //所以需要第一个节点取消的时候去唤醒第二个节点,所以取消操作的时候,如果前一个节点是头结点,需要唤醒下一个可用结点
            return true;
        //前一个node是取消的
        //就算做过清理了,清理后可能被置为取消.cas为signal没有成功
        if (ws > 0) {
            //如果前置节点是取消状态,那么清理掉前面连续的取消结点.
            //如果前面的node被取消,并且在做自我清理,不会影响到这里,当然自我清理可能失败
            //一个节点不能从取消回到非取消状态,那么只要是取消就可以跳过
            //自我清理失败的结果是:prev指向了前面,而前面对应的next没有指向取消结点的下一个结点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //这里是唯二能够真正清理掉取消结点的地方
            //还有一个是cancel的时候,把前一个可用结点的next做cas操作指向后面
            pred.next = node;
        } else {
            //这时候,这个节点可能在取消,那么ws就会是cancel,就不能设置为signal
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        //那就再来一次
        return false;
    }


    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
        //置空 成为空node
        node.thread = null;

        // 跳过被取消的node
        // 但是停下的地方可能一会儿也被取消
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        //pred取消了, predNext可能是pred
        //也可能一会儿发生改变
        Node predNext = pred.next;

        //置为取消状态
        //可能有别的结点把这个结点设置成了signal,取消就是了
        //但这时候后面的节点先判断是signal然后去park了,结果这里又被设置为取消,那么后面的节点可能需要被唤醒
        //是否要唤醒,依赖于前方是否还有可用结点,如果有的话,那就不需要了,如果没有,那就是需要的
        node.waitStatus = Node.CANCELLED;

        //如果是尾结点,将最后一个非取消结点置为尾结点
        //如果有新的线程入队,cas将会失败.但是入队操作会帮我们把node的next指向了新的tail,这样就把node留在了队列里.
        //不过新的入队线程会马上把我们给跳过清理掉
        //这里也可能aba,新线程入队,马上被取消出队,没有副作用
        //pred这时候可能是取消的
        if (node == tail && compareAndSetTail(node, pred)) {
            //把尾结点的next置为null
            //如果这时候又有新的入队,则next已经不是predNext,将会失败
            //或者pred取消了,指向了自己
            //或者pred取消前新线程入队,新线程还未将oldTail的next指向新node,pred取消后next指向自己,然后又被新线程指向了新的tail
            pred.compareAndSetNext(predNext, null);
        } else {
            int ws;
            //如果前一个有效结点不是头结点 尝试把前一个有效结点置为signal
            //如果前一个节点这时候取消了,那么就会失败
            //能置成功或者本来就是signal的,说明前面这个结点没有被取消,否则就不清楚前方什么情况
            //那么需要调用unpark,指不定前方没有节点可用了
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL)))
                && pred.thread != null) {
                //进到这里只是为了确定,前一个节点此时没有被取消
                //如果进来后前一个结点要取消或者取消到一半,不会影响到后面的操作
                //每一个取消操作,都可能调用unpark,所以我只保证前面有节点可用,我就不会去unpark
                //如果没有进来,说明我不知道前面是否有节点可用,保守起见,我调用unpark
                Node next = node.next;
                //尝试摘掉自己和前面所有的取消结点
                if (next != null && next.waitStatus <= 0)
                    //pred的next可能指向了自己,也可能指向了更后面的节点
                    //1 pred取消后指向了自己
                    //2 有更后面的取消结点比我更先操作pred,那么pred会指向更后面,比如pred成了尾结点
                    pred.compareAndSetNext(predNext, next);
            } else {
                //前一个节点可能是head,那么需要唤醒下一个node
                //这里的这个操作是必要的
                //只靠release来唤醒是不够的 如果刚好有新线程在入队,那么release操作就无法遍历到新线程去唤醒
                //另一方面,不清楚前方状况,全局看一下
                unparkSuccessor(node);
            }

            //如果这时候后面的节点都被取消了,这个结点成了尾结点,这个next可能在新线程入队的时候又被指向了新的tail
            //也可能在shouldPark往前跳过取消结点时,赋值给下一个结点
            //如果这里不做这个操作,同时acquireQueued函数里,head.next不被置为null,next遍历也会是可靠的??
            //只要next不会指向前面的结点形成循环,不会被置为null
            node.next = node; // help GC
        }
    }

唤醒

release后,将自己置为signal,唤醒下一个node.如果没有人在排队,直接返回.

tryRelease

    protected final boolean tryRelease(int releases) {
        //和tryAcquire相反的操作 就不会区分公平非公平了,因为只有一个线程获取了锁
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            //先置空线程
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

    public final boolean release(int arg) {
        //只有之前acquire成功的才能tryRelease成功
        if (tryRelease(arg)) {
            Node h = head;
            //如果acquire是在队列空的时候,不用排队,也就没有head
            if (h != null && h.waitStatus != 0)
                //如果有队列的话,唤醒head下一个node
                unparkSuccessor(h);
            return true;
        }
        //如果这时候有人排队了,要靠它自己入队时的自旋来获取锁
        //就不是触发式的
        return false;
    }

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;

        //清理状态
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);

        //这里node.next可能会是node自身
        Node s = node.next;
        //下一个结点是个空或者在取消
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从后往前遍历找到最前面的
            //如果是从前往后去找,找到某个节点后,这个结点被取消,然后next指向了自己,就悲剧了.
            //如果有新线程入队,这个新线程不能被遍历到,这点会带来一个隐患,是node自旋无法解决的:
            //考虑p赋值为tail后, 新线程入队,然后新线程查看自己是不是第一位
            //不是第一位,然后尝试往前跳过取消结点,发现依然不是第一位
            //这时候新线程被挂起,接着前面的节点都取消了
            //这时候除了取消操作里去唤起,没有别的操作能唤起这个线程了
            //所以cancel的函数里是需要调用本函数的
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        //可能队列空了,可能是新线程没有被遍历到
        if (s != null)
            LockSupport.unpark(s.thread);
    }

原文链接

原文链接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值