AQS底层源码浅析1(Deprecated)

该文逻辑 不是很清晰,不建议浏览,建议看这一篇

AQS基础结构

以下内容是自己学习AQS的一些总结,如果有不同见解的,欢迎评论一起探讨,如果有理解不到位的,希望能够帮我指正。AQS的代码设计真的很精巧,可能一个判断就已经包含了n多种含义。但理解了它,会让自己对并发的理解更上一个层次。

首先,我们来看一张AQS的同步队列结构图:
AQS的同步队列结构图
state是一个锁标志位,是能否上锁,是否排队等所依托的一个重要属性。不同的锁对于该标志的实现也是有所区别的,例如ReentrantLock和ReentrantReadWriteLock;
Node是整个AQS的其中一个关键类,封装了队列中节点的关键信息,我们这里可以先只关注prev和next两个引用属性;prev是引用了前一个节点,next是引用了下一个队列节点,简而言之,AQS的同步队列就是利用双向链表构建的一个队列。

上锁过程涉及到的方法

相应的个人理解都写在代码注释上。

acquire(int arg) 是lock()实际调用的方法

public final void acquire(int arg) {
//tryAcquire尝试获取锁,如果获取不到则会进入acquireQueued进行排队
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt(); // 设置中断位
}

AQS的tryAcquire是由子类去实现的,这里我们以ReentrantLock的公平锁的tryAcquire(arg)为例

/**
 * 公平版本的tryAcquire().  
 * 除非重入、没有等待者,处于第一个waiter这三种情况,否则不授予访问权限。
 * 逻辑:
 *   1、获取当前线程和当前队列的计数器
 *   2、如果锁的状态是0,则属于自由状态
 *      如果锁的状态非0,但是exclusiveOwnerThread是当前线程,则属于重入状态
 *      两个都不是,排队去吧....
 */
protected final boolean tryAcquire(int acquires) {
    
    final Thread current = Thread.currentThread();
    int c = getState();
    // c==0代表此时的锁为自由状态
    if (c == 0) {
        // 判断是否需要排队,因为虽然c==0,但是有可能存在一个线程在当前线程之前就已经走到了这一步
        if (!hasQueuedPredecessors() &&
            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;
}

tryAcquire在state=0时的处理

先从hasQueuedPredecessors()入手

public final boolean hasQueuedPredecessors() {

        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        /**
           h!=t会分为三种情况:
              1、如果h==t,则可能说明队列没有初始化,返回false,所以取反后为true,tryAcquire()也会走cas尝试获取锁
              
              2、如果h!=t(非一个节点),此时s是队列当中第一个排队节点
                 例如买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;
                 h作为第一个节点,也分为两种情况,虚拟或者持有锁的节点
                 
                 如果s为空,则队列只有一个节点,因为如果大于1个节点,则肯定不存在h.next == null的情况;
                 如果队列长度是大于1的,则s==null不成立,此时为||,则会继续判断是否s节点的线程和当前的执行线程是否为同一个
                     
                     s.thread != Thread.currentThread()又分为两种情况:
                        如果为true,则并非同一个线程,则整个方法执行结果为true,需要排队;
                        此时的情况就是队列不为空,(h.next)有人在排队,新来了一个人,这个人和排队的不是一个人,所以继续排队
                        如果为false,则为同一个线程,整个方法返回false,也就是不需要排队了
                整体的意思是,如果当前队列元素大于1,说明有人在排队,当前线程在进入排队前会去验证第一个排队的线程是不是自己,
                如果是的话,则直接进行尝试获取锁,因为第一个等待线程是处于park状态的,所以不可能说会有重入,因为重入是首先
                线程是已经处于获取锁的状态;
              
              3、队列只有一个节点,头尾指向同一个;按照AQS的原理,则这个节点应该是虚拟节点,即当前执行线程;整个方法返回false;
                 所以此时是不需要排队的,因为会通过自旋获取锁,获取不到才会进入排队并park;

         */
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

当hasQueuedPredecessors()返回false,取反后则进入compareAndSetState设置锁状态,当设置成功后,再接着将exclusiveOwnerThread设置为自身,并返回true,而如果设置失败,则返回false。

tryAcquire在当前线程为执行线程时的处理

以下这段代码是在上面tryAcquire()方法中的。

            else if (current == getExclusiveOwnerThread()) {
                //如果当前线程为执行线程,则先拿到进行处理重入后的state
                int nextc = c + acquires;
                // 如果state小于0,则报错
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 否则设置state并返回true
                setState(nextc);
                return true;
            }

如果tryAcquire返回false,即尝试获取锁失败,则会走acquire()方法中后续的acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这两个方法。

/**
 * 通过当前线程和给定的模式创建节点并且入队
 * @param mode 独占模式(Node.EXCLUSIVE),共享模式(Node.SHARED)
 * @return 返回新节点
 */
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //获取尾节点
    Node pred = tail;
    // 如果尾节点不等于空,则说明队列已经初始化了,则有如下操作
    //     1、将新节点的prev设置位原先的尾节点
    //     2、CAS将新节点设置为队列的尾节点
    //     3、如果CAS成功,将原先的尾节点的next设置为新节点,返回新节点
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 队列未初始化,或者新节点进队失败则会进行enq()操作
    // 至于为什么会失败,有可能是在某一瞬间存在t1和t2都在往队列中addWaiter,如果t1先入队成功,则此时tail节点已经
    // 改变了,所以CAS失败
    enq(node);
    return node;
}

enq(final Node node) 方法带有循环操作,请注意查看循环体

/**
 * 插入节点到队列中,如果有必要则进行队列初始化
 * 1、进入死循环自旋,记住这里是死循环
 * 2、获取尾节点
 *    如果尾节点为空,则队列未初始化,新建一个执行节点
 *    如果队尾节点不为空,则说明队列已经初始化了,直接入队即可
 * 3、将新节点的prev引用指向队尾节点,再用CAS 的方式将当前节点设置为尾节点,再将原先的尾节点的下一个节点设置为新节点
 * 4、返回当前节点位置的前一个节点
 * @return 节点的前一个节点
 */
private Node enq(final Node node) {
    // 如果队列为空,则创建一个占位头节点,该节点Thread为空
    // 然后重新执行第二遍循环,将新队节点加入队列的第二个节点
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 如果队列本来就不为空,则将新节点的前一个节点的引用设置为原先的尾节点
            // 并通过CAS将新节点设置为尾节点,返回队列未修改前的那个尾节点,这个返回没什么实际意义
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued(final Node node, int arg)用于设置线程休眠,并对处于队列第一个等待节点的节点进行尝试获取锁操作,该方法带有自旋操作。

/**
 * @param node 按照原先的逻辑,这里的参数就是在addWaiter中创建的那个新节点
 * @param arg the acquire argument
 * @return 返回true的话则说明中途被中断过
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;             // 标记是否竞争锁失败
    try {
        boolean interrupted = false;   // 标记是否等待过程中被中断过,用于最后的自我中断

        // 请记住,这里是一个死循环,死循环,死循环操作
        // 如果线程在这个循环里park()了,下次醒来还会重新循环操作一遍
        for (;;) {
            // 返回上一个节点, 为空时抛出NPE
            final Node p = node.predecessor();
            // 这里要先记住节点是已经存在于队列中了,已经存在于队列中了,已经存在于队列中了
            // 如果刚才入队的节点的前一个节点是头节点,则入队的节点应该是队列中第一个
            // 在进队之后,节点是第一个等待节点,则还会进行一次尝试获取锁操作
            
            // 与之前的tryAcquire的含义不同,这里是表示进队之后如果是第一个节点,
            // 就再看一下获取锁的线程节点是否已经release,如果已经release了,就不休眠了,直接获取锁
            // 而如果当前节点的前一个节点并非头节点,则说明当前节点前面节点还没执行,怎么也轮不到当前节点获取锁啊...
            
            if (p == head && tryAcquire(arg)) {
                // setHead主要就是将目标节点设置为头节点,将prev,thread设置为空
                setHead(node);
                // 此时是新节点成为了头节点,所以后续无节点
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            // 如果此时的前一个节点并非头节点,则也不存在抢锁的必要,因为前面的都还没执行,哪轮得到新节点
            // 同时,如果前一个节点是头节点,但是抢锁失败也会来到这里,置于为什么会枪锁失败,
            // 还是因为可能会有一个线程先后的问题
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire方法主要有两个作用,一个是对于已取消节点的整理,方便GC回收;另一个是对于节点间信号的控制;同时,当且仅当目标节点的前一个节点的ws状态已经为SIGNAL时才回返回true。

/**
 * 作用1、节点整理,跳过取消节点后将新节点设置到第一个正常节点后面
 * 作用2、信号控制
 * @param pred node's predecessor holding status
 * @param node the node
 * @return 除非前一个节点的ws状态为SIGNAL,不然不会返回true
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;  // 前一个节点的状态

    // SIGNAL(-1):表示后继结点在等待当前结点唤醒。
    // 后继结点入队时,会将前继结点的状态更新为SIGNAL
    // 如果前一个节点是SIGNAL,已经设置为后继节点在等待前置节点唤醒的状态的话,就安心休息了
    if (ws == Node.SIGNAL)  
        return true;

    // 如果状态大于0,则处于CANCELLED状态,说明前一个节点处于取消状态
    // 那么此时节点不应该放在取消节点后啊,
    // 就好比,排队买票,有人排到一半中途出去吃饭,那后面的人肯定就往前挪,不可能给他保留位置等他吃饭回来再继续往前走
    if (ws > 0) {

         // 先记住,node本来是队列的最后一个节点,同时它的next=null

         // 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         // 那些放弃的结点,由于被node“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         // 原队列 [n1]<-->[n2]<-->[n3]<-->[n4]-->null,若n3和被取消,则队列变成 [n1]<-->[n2]<-->[n4]-->null
        do {
            // 原本的代码是: node.prev = pred = pred.prev;  相当于下面的代码
            pred = pred.prev;
            node.prev = pred;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前一个节点必须为0或者传播状态,将前置节点设置为SIGNAL后还不能park,
        // 需要重试以确定前置节点真的被设置成了SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt()这个其实就是调用了unsafe的park()使线程进入睡眠,当然,如果线程被重新唤醒,那也是从这个位置开始继续执行


private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 最终休眠位置
    return Thread.interrupted(); // 返回线程的中断状态
}

总结

请牢记,这里的tryAcquire是用ReentrantLock的FairSync的tryAcquire进行介绍的。不同的锁在这方面的实现是不同的,相对来说,ReentrantLock的FairSync提供的比较好理解。

到这里,对整体的执行逻辑进行流程总结,详细逻辑点还是需要查看上面的源码解析:

acquire到addWaiter的整体流程
acquire到addWaiter的整体流程
从addWaiter到acquireQueued的整体流程
从addWaiter到acquireQueued的整体流程
简而言之就是:

  1. lock会调用acquire方法去进行获取锁。
  2. acquire方法会涉及到判断是否队列有人(公平情况下),如果有人的话是否我是排队的第一个,如果是的话就先不睡了(详细情况看上面),而当真的竞争不到锁,则将自己加入到队列中。
  3. 再由acquireQueued去判断需不需要休眠,如果此时是第一个等待节点,那就再去竞争锁。这里是有一个自旋操作的,当需要休眠并且确定前一个节点已经设置SIGNAL了就去睡觉。最终的休眠是调用UnSafe.park()实现的。

在从addWaiter到acquireQueued的整体流程这张图中还涉及到另一个方法,cancelAcquire(),实际上这个方法设计到了AQS同步队列的出队操作,有关这方面的理解我会留到第二篇博客中介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值