AQS的核心方法-acquire()解析

以下是AQS的acquire()方法源码:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

一步步拆解,先看下tryAcquire()方法,这个方法是由AQS子类来做具体实现的,暂不关注,重点关注AQS的几个模版方法。

addWaiter()方法

先看addWaiter()方法,参数是一个Node对象,那么就先看下Node对象所对应的类是怎样的定义:
在这里插入图片描述
可以发现Node类是AQS类的一个内部类,有若干的方法和属性,这时候还无法知道这些方法和属性的作用是什么,所以继续跟进addWaiter()方法中

addWaiter()方法

先看看addWaiter()方法的源码:

private Node addWaiter(Node mode) {
        //构造一个Node对象,参数是当前线程对象以及mode对象,mode表示该节点的共享/排他性,值为null为排他模式,不为null则共享模式
        Node node = new Node(Thread.currentThread(), mode);
        //拿到AQS的尾节点 
        Node pred = tail;
        //如果尾节点不为空
        if (pred != null) {
            //先把新加入的节点的前驱节点设置为尾节点,新加入的节点会加入队列的尾部
            node.prev = pred;
            //通过CAS操作把新节点设置为尾节点,传入原来的尾节点pred和新节点node做判断,保证并发安全
            if (compareAndSetTail(pred, node)) {
                //把新节点设置为原来尾节点的后继节点
                pred.next = node;
                //返回新节点,这个节点里封装了当前的线程
                return node;
            }
        }
        //尾节点为null,则将新节点加入队列
        enq(node);
        return node;
    }

enq()方法

继续跟进enq()方法,看看它是怎么将节点加入队列的:

private Node enq(final Node node) {
        //这是一个死循环,不满足一定的条件就不会跳出循环
        for (;;) {
            //获取AQS尾节点
            Node t = tail;
            //如果为null,其实这是个循环判断,可能下次再做判断时,就有其他线程已经往队列中添加了节点,那么tail尾节点可能就不为空了,就走else逻辑了
            if (t == null) { // 必须初始化
                //则新建一个Node对象,通过CAS设置成头节点,这个head其实是冗余节点
                if (compareAndSetHead(new Node()))
                    //把尾节点设置为head
                    tail = head;
            } else {
                //尾节点不为空,则把尾节点设置为新节点的前驱节点
                node.prev = t;
                //做CAS操作,把新节点设置为尾节点
                if (compareAndSetTail(t, node)) {
                    //CAS成功后,则把新节点设置为原来尾节点的后继节点
                    t.next = node;
                    //返回新节点,这个节点里封装了当前的线程
                    return t;
                }
            }
        }
    }
    
 //这个方法关注一下,可能会豁然开朗,这就是当尾节点尾null时,需要设置头节点,来做个初始化的方法,把头节点从null设置为update对象
 private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }    

所以enq()方法是很有意思的,它通过死循环的方式,来保证节点的正确添加,可以发现只有当新节点被设置为尾节点时,当前线程才能从enq()方法返回,然后再配合上CAS,是不是有一种很抽象的感觉,节点一个一个的被加到队列中,一个一个的接着被设置为尾节点,并发的操作,串行的感觉。

所以通过addWaiter()方法,竞争同步状态失败的线程就被成功的加入到对列的尾部了

acquireQueued()方法

再接着看看acquireQueued()方法:

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取node前继节点
                final Node p = node.predecessor();
                //如果node的前继节点是头节点,同时当前线程获取同步状态成功
                if (p == head && tryAcquire(arg)) {
                    //那么把当前节点设置为头节点,同时把当前节点的前继节点置为null
                    setHead(node);
                    //再把前头节点p的后继节点设置为null,这样前头节点就没有任何引用了,帮助GC,清理前头节点
                    p.next = null; // help GC
                    //这里设置把标志位failed设为false,说明成功走到了这步逻辑
                    failed = false;
                    //要注意,无限循环只有这个出口,返回interrupted后,跳出循环,这个返回值就表示了要不要中断
                    return interrupted;
                }
                //当node的前继节点不是头节点或者获取锁失败时,判断是否需要阻塞等待,如果需要等待,那么就调用parkAndCheckInterrupt()方法阻塞等待
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果线程被中断的,那么重新设置中断状态为true,然后返回表示需要中断
                    interrupted = true;
            }
        } finally {
            //如果出现不正常情况,failed标志位还没被置为false,就会取消
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire()方法

深入进去看看shouldParkAfterFailedAcquire()方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 
        //获取前继节点状态
        int ws = pred.waitStatus;
        //如果等于SIGNAL,则直接返回true,表示要阻塞
        if (ws == Node.SIGNAL)
            //要注意只有这个分支会返回true 
            return true;
        //如果状态大于0,表示前继节点需要做的请求被取消了,   
        if (ws > 0) {
            //这个分支循环做一件事,把所有的被取消的前继节点移除,直到waitStatus值不再大于0,然后把这个没有被取消的节点和node节点连接起来
            do {
                node.prev = pred = pred.prev;
                //上述代码转换成如下模式可能会更好理解,其实就是反复取值赋值
                (node.prev = pred;
                 pred = pred.prev;)
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //通过CAS设置前置节点的状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //返回false
        return false;
    }

这里需要再回顾acquireQueued()方法,shouldParkAfterFailedAcquire()方法的结果一旦返回false,那么acquireQueued()方法的死循环就不会跳出,还是会继续检查node的前继节点是否是头节点,同时当前线程获取同步状态是否成功,而如果shouldParkAfterFailedAcquire()方法的结果是true,就会调用parkAndCheckInterrupt()方法:

private final boolean parkAndCheckInterrupt() {
        //LockSupport.park()实现阻塞等待,等着unpark和interrupt叫醒他
        LockSupport.park(this);
        //检查是否被中断,清除中断状态,并返回中断标志
        return Thread.interrupted();
    }

到这步可以发现,acquireQueued()方法的死循环逻辑配合上shouldParkAfterFailedAcquire()方法的去除取消节点和设置SIGNAL状态的操作,整个队列慢慢的会趋向于:

  • 只要不是头节点,那么其他的节点都是返回true,表示需要中断,这都是shouldParkAfterFailedAcquire()方法的功劳
  • 只要不是尾节点,那么其他的节点状态都是SIGNAL,因为只有节点状态是SIGNAL,才会返回true,这也是shouldParkAfterFailedAcquire()方法的功劳

最后如果acquireQueued()方法中出现了异常,则会调用cancelAcquire()方法来取消节点。

selfInterrupt()方法

最后再看看selfInterrupt()方法:

static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

这个线程没能获取到同步状态,同时acquireQueued()方法返回true,那么就会调用selfInterrupt()方法来设置当前线程的中断状态。

acquire()方法流程图

在这里插入图片描述

总结

看完源码分析,做到大致熟悉,然后以代码为切入口理解流程图即可。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值