AQS源码分析

前言

最近非常着迷阅读源码,周末闲来无事,又把AQS【AbstractQueuedSynchronizer】过了一遍。

网上有很多这方面的文章,为什么还要自己去写呢?一是为了加深记忆,二是为了方便日后查看,三是锻炼归纳总结的能力。有缘的读者可以大致看一下。或许有些帮助。

对于初学者来说可能并不熟悉AQS,它是java.util.concurrent包下的工具类,日常开发中,很少有人去使用它,但是ReentrantLock、CountDownLatch都用过吧,至少听过说吧。它们都是基于AQS实现的。

为了能更深入的理解锁的概念,我觉得有必要深入了解这些工具类。

AQS概览

首先对AQS有个总体认识,它都有什么功能,如何作为抽象类给其他类提供帮助。

AQS提供两种锁的解决方案:

  1. 独占锁:在锁的区域内(可以理解为日常开发中sychronized包裹的区域,或者说lock--unlock的区域),只允许一个线程进入。ReentrantLock就是独占锁。
  2. 共享锁:在锁的区域内,允许多个线程进入。CountDownLatch、Semaphore都是共享锁。

那么如何利用AQS来实现独占锁/共享锁呢?

大体过程:新建一个类TestLock,然后在TestLock内部创建一个内部类TestSync extends AbstractQueuedSynchronizer,TestSync 重写AQS的tryAcquire/tryAcquireShared和tryRelease/tryReleaseShared。接着在TestLock类中创建testlock方法写一些获取锁前的判断逻辑,最后调用AQS的acquire/acquireShared方法来获取锁。创建testunlock方法写一些锁的释放逻辑最后调用AQS的release/releaseShared方法来释放锁。

其中tryAcquire和tryAcquireShared分别代表获取独占锁的方法和获取共享锁的方法。release/releaseShared代表释放独占锁/释放共享锁的方法。

AQS可以看作是一个锁引擎,我们可以利用它来实现定制锁。

说了这些可能该懵逼的还是一脸懵逼,下边通过解读ReentrantLock,来一步步分析AQS是如何帮助ReentrantLock来实现独占锁的。

ReentrantLock分析

先看下它的类图

 

日常使用ReentrantLock的流程。

        //默认构造方法是非公平锁
        ReentrantLock lock=new ReentrantLock();
        //加锁,用于应对锁区域的高并发,只能有一个线程进入锁区域,其他线程等待
        lock.lock();
        //执行业务代码。。。。
        //。。。。
        //释放锁
        lock.unlock();

上边第一句代码就是创建ReentrantLock对象,调用默认构造方法,默认构造方法内部创建一个NonfairSync非公平锁,NonfairSync是ReentrantLock的内部类,它最终继承自AQS,重写了tryAcquire和tryRelease等方法。所以概括起来,第一句就是创建了一个非公平锁对象。

第二句就是加锁了,点进去看一下源码,最终调用的NonfairSync的lock方法:

     final void lock() {
            //通过CAS操作,假如AQS中state状态为0,则设置state为1
            //如果设置成功,则代表当前线程成功获取到锁
            //如果设置state失败,则调用AQS的acquire方法去获取锁。
            //在这里也体现了非公平锁的特性,就是新来一个线程要获取锁,首先会尝试CAS获取锁,这样就失去了公平性,它可以同正在排队的线程一同竞争获取锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

跟我上边说的流程一样,写一些锁的逻辑最后调用AQS的acquire/acquireShared方法来获取锁。具体逻辑参考代码中的注释,到这里引出了AQS中state成员变量。它的作用可以理解为锁的状态,假如为0,则代表当前没有锁,线程可直接进入锁区域,假如state大于0,代表当前存在锁竞争,成功获取到锁的线程会进入锁区域,其他线程需要排队等候。

在lock方法中,假如当前state大于0,存在锁竞争,则会调用AQS的acquire方法,代码如下:

   public final void acquire(int arg) {
        //简单的一个if判断,其实是执行了三个方法。
        //首先调用tryAcquire方法,tryAcquire方法上边说过,是实现类自己实现的获取锁方法,
        //假如获取锁成功,则直接返回。
        //假如获取锁失败,会调用addWaiter(Node.EXCLUSIVE)方法,该方法将当前线程添加到AQS的等待队列中,
        //接着调用acquireQueued方法,该方法内是一个死循环,正常情况下循环两次后线程会进入等待阻塞状态
        //直到其他线程唤醒它,继续执行死循环,直到满足条件跳出循环。
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //如果在线程等待过程中,线程状态被设置为interupt状态,会进入if中,调用线程的interupt方法,调用该方法后,会抛出异常。
            selfInterrupt();
    }

acquire方法是AQS中比较重要的方法,详细的流程参见上边的注释。针对代码中调用的几个方法,我们再深入分析一下,先看下

tryAcquire方法,上边说了该方法是实现类实现的,那就看下ReentrantLock中非公平锁NonfairSync内是如何实现的该方法:

   protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

 我们发现内部调用的nonfairTryAcquire方法,看下nonfairTryAcquire方法:

        final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取AQS的state
            int c = getState();
            //如果state为0,代表没有加锁,尝试使用CAS来获取锁,就是通过修改state来获取锁
            if (c == 0) {
                //获取设置state成功,说明获取到锁,并设置当前线程拥有该锁。
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果state不为0,说明已经有线程获取到锁,如果获取到锁的线程是当前线程,则直接修改state的状态。
          //这种实现代表ReentrantLock是可重入锁。
            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;
        }

概括tryAcquire方法的作用,成功获取锁则返回true,获取锁失败,则返回false。

接着回到acquire方法,看完了tryAcquire方法,看下addWaiter方法,addWaiter方式是在tryAcquire方法获取锁失败后执行的:

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        //创建AQS中双向链表节点,该节点代表当前线程,该节点的下一个节点为mode,mode值是Node.EXCLUSIVE=null,
        Node node = new Node(Thread.currentThread(), mode);
        //获取链表的尾节点
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //如果尾节点不为null,设置当前节点为新的为尾结点,
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果设置失败,则进入死循环,一直尝试将当前节点添加到队列,直到添加成功
        enq(node);
        //返回当前节点。
        return node;
    }

addWaiter方法中注意enq(node)方法,当执行到这个方法时,说明队列是空的。看下方法内是如何做的:

    private Node enq(final Node node) {
        //死循环
        for (;;) {
            //获取队列尾节点
            Node t = tail;
            //如果为节点为null,则初始化队列。
            if (t == null) { // Must initialize
                //初始化队列,头节点为new Node不包含线程,如果创建队列成功,则头尾节点都为new Node
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //第二次循环,因为已经初始化队列,尾节点不为null,走else
                //设置当前线程node为新的尾结点,它的前节点就是new Node
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

接着回到acquire方法,看完了tryAcquire和addWaiter方法,看下acquireQueued方法:

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前节点
                final Node p = node.predecessor();
                //如果前节点是头节点,并且获取锁成功的话,设置当前节点为头节点。返回
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //这个if判断包含两个方法,首先执行shouldParkAfterFailedAcquire方法,如果返回true则继续执行parkAndCheckInterrupt方法
                //shouldParkAfterFailedAcquire方法是判断前节点的状态,假如为Node.SIGNAL则返回true。
                //parkAndCheckInterrupt方法阻塞当前线程,直到其他线程唤醒当前线程,代码才会继续往下执行。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //假如当前线程在获取锁之前出现异常,则取消当前线程获取锁
            if (failed)
                cancelAcquire(node);
        }
    }

针对acquireQueued方法中shouldParkAfterFailedAcquire方法,看下内部是如何实现的:

//shouldParkAfterFailedAcquire方法是判断前节点的状态,假如为Node.SIGNAL则返回true。    
//如果前节点为Node.CANCELLED。则跳过这个节点找前节点的前节点,直到目标节点不是Node.CANCELLED状态。
//并设置目标节点为当前节点的前节点(相当于把CANCELED节点从队列中移除了)。返回false。
//等到下次循环进入该方法,当前节点的前节点保证不是CENCELED状态了,接着判断前节点是否为Node.SIGNAL,
//如果不是,则设置前节点状态为Node.SIGNAL。
//等到下次循环进入该方法,前节点一定为Node.SIGNAL,返回true,执行parkAndCheckInterrupt方法。
//所以该方法,最快的话,一次循环返回true,最慢3次循环返回true
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前节点的状态。状态如果没有赋值,默认为0.
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

对于Node的状态,这里多说一嘴,一共4个状态:

        /** waitStatus value to indicate thread has cancelled */
        //当节点状态为CANCELLED时,会被程序从队列中删除
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        //当节点状态为SIGNAL时,如果程序从队列中唤醒
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        //当前文章用不到该状态,
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        //当前文章用不到该状态,
        static final int PROPAGATE = -3;

 shouldParkAfterFailedAcquire方法中,状态>0代表是Node.CANCELLED。如果状态没有被赋值过,则默认为0.

以上基本对ReentrantLock类的加锁过程分析了一遍。

概括一下流程:

  1. 调用lock方法获取锁,
  2. 获取锁成功(获取锁成功还是失败由谁判定?=》tryAcquire方法)则执行锁内部代码,获取锁失败则进入等待队列。

参考流程图

说完了获取的流程,接着说下释放锁的流程,看下unlock的代码:

  public void unlock() {
        sync.release(1);
    }

代码很简单,直接调用AQS的release方法:

    public final boolean release(int arg) {
        //if判断,调用实现类tryRelease方法,判断是否能成功释放锁,如果释放成功,
        //进入if,如果队列中头节点不为null,并且状态不为默认值0,则调用唤醒线程方法。将阻塞的线程唤醒继续执行。
        //至于唤醒的是哪个线程看下边对unparkSuccessor的分析
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

代码注释已经写的很清楚了,看下实现类的tryRelease方法:

        protected final boolean tryRelease(int releases) {
            //定义变量C,为释放锁后的state值。为state-1
            int c = getState() - releases;
            //如果当前线程,不是拥有锁的线程,抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果c为0,代表没有锁了,设置锁中对应的线程变量为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //修改state
            setState(c);
            //只有c为0时才会返回true。
            return free;
        }

接着看下unparkSuccessor方法:

    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.
         */
        //获取成员变量node状态
        int ws = node.waitStatus;
        //如果状态小于0,通过CAS设置其等于0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        //获取头节点的下一个节点
        Node s = node.next;
        //如果头节点下节点为null或者下节点的状态为CANCELED,则从尾部开始尝试遍历,找到状态小于0的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //通过上一步的筛选,找出符合要求的节点,唤醒该节点的线程。
        if (s != null)
            LockSupport.unpark(s.thread);
    }

 在unparkSuccessor方法中,到底唤醒的是队列中的哪个节点?

可以概括为:先判断头节点的下一个节点是否为null或者状态为CANCELED,如果是,则从队列尾开始向头方向找,直到找到一个不为CANCELED节点作为唤醒节点,如果不满足条件,则唤醒头节点的下一个节点。当然唤醒前需要做判断,必须节点不为null才会被唤醒。

以上就是unlock方法的执行流程。

总结

以上只是对AQS独占锁的分析,共享锁的实现跟独占锁略有区别,只要摸清了一种锁的实现方式,另外一种就很容易看懂了。

通过对ReentrantLock分析,从中学到了AQS引擎的结构和它的核心方法。整个流程其实很简单,但是每一步都比较繁琐。

因为是针对多线程,方法中有许多变量都是公用的,如state,head,tail等。当多个线程并发访问时,从头捋清楚代码是如何执行的还是比较困难的。这篇文章,也只是按照我的思路来考虑并发,许多细节可能未考虑周到。但整体思路是没有问题的。不妨碍学习AQS的精髓。

由于多线程的干扰,调试也比较困难,所以对于锁,能满足需求的尽量使用官方的工具类,自己实现难免会考虑不周,出现各种bug。

Java中的AQS(AbstractQueuedSynchronizer)是实现锁和同步器的一种重要工具。在AQS中,一个节点表示一个线程,依次排列在一个双向队列中,同时使用CAS原子操作来保证线程安全。当多个线程对于同一资源竞争时,一个节点会被放置在队列的尾部,其他线程则在其之前等待,直到该资源可以被锁定。 当一个线程调用lock()方法进行锁定时,它会首先调用tryAcquire()方法尝试获取锁。如果当前资源尚未被锁定,则该线程成功获取锁,tryAcquire()返回true。如果当前资源已被锁定,则线程无法获取锁,tryAcquire()返回false。此时该线程就会被加入到等待队列中,同时被加入到前一个节点的后置节点中,即成为它的后继。然后该线程会在park()方法处等待,直到前一个节点释放了锁,再重新尝试获取锁。 在AQS中,当一个节点即将释放锁时,它会调用tryRelease()方法来释放锁,并唤醒后置节点以重试获取锁。如果当前节点没有后置节点,则不会发生任何操作。当一个线程在队列头部成功获取锁和资源时,该线程需要使用release()方法释放锁和资源,并唤醒等待队列中的后置节点。 总之,AQS中的锁机制是通过双向等待队列实现的,其中节点表示线程,使用CAS原子操作保证线程安全,并在tryAcquire()和tryRelease()方法中进行锁定和释放。该机制保证了多线程环境下资源的正确访问和线程的安全执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值