Java J.U.C 中 AQS 子类 ReentrantLock 源码分析(一)

写文章不易,转载请标明出处。

同时,如果你喜欢我的文章,请关注我,让我们一起进步。

一、概述

对于 Java 中的 JUC 包大家应该都是非常熟悉的,JUC 的全称是 Java.util.concurrent ,翻译过来也就是 Java 并发编程工具类包,在这个包中有许多在我们并发编程过程中经常使用到的线程安全的容器类和同步锁等一些组件,而在这个包中很多的线程安全都是基于基础底层的 AQS 来实现的,而 AQS 也就是 AbstractQueueSynchrogazer ,翻译过来就是抽象队列同步器,通过这个类的命名方式,我们就可以猜到它是一个抽象类。

在其使用的过程中,AQS 也是作为一个抽象的基类,通过模板方法设计模式来实现子类最基础的线程安全,并将具体的一些方法如 tryAcquire 或者 tryRelease 等交由子类去实现,让子类根据自身功能的需要去定制实现相关的细节方法,而如果我们在这里直接调取抽象基类 AQS 的一些方法则会直接抛出异常。

因为 AQS 的实现子类非常的多,并且一些具体的获取锁和释放锁的实现都延迟到了子类中,因此在选择源码的阅读时,选择了比较常用的 ReentrantLock 。这个锁是基于 Java 的一款显示锁,我们可以动态的对代码进行加锁和解锁的操作,在编码的过程中具备更高的灵活性(与之相对的是使用 C++ 在 JVM 中实现的 Synchrogazed 隐式锁,该锁完全由底层的 C++ 代码实现,内嵌于 JVM 中,加锁和解锁的过程完全由 JVM 来进行控制,我们无法通过编写代码来进行干预)。

对于 ReentrantLock 的使用我们一般就是直接通过 lock 方法来进行加锁,当需要同步的代码执行完成后再使用 unlock 来进行解锁,所以我们接下来代码的分析主要就从这里入手(示例代码没什么太大作用,就是提供一个进入 ReentrantLock 的入口,我们从这里开始阅读源码)。

最后说明一些编写这篇博文的意义,因为自己在网上也查找过比较多关于 AQS 的资料,也阅读了一些书籍上的文章 ,但是普遍都感觉对于源码的分析过于零散,大多是将代码拆的零七八碎,把每一个方法都单独拎出来说明其入参的含义或者代码的逻辑,给人一种舍本逐末的感觉,个人认为源码的阅读在于理解源码编写时的一种编程的思想,而这种思想应当是连续的,而不应该被肢解,所以我会尽量通过自己的理解并结合相关的资料以顺序执行的方式来对这部分源码的执行主流程进行分析,希望可以帮助到大家。

二、示例代码

public class AQSDemo {
    public static void main(String[] args) {
        // 1.创建锁
        final ReentrantLock lock = new ReentrantLock(true);
        Thread t= new Thread(){
            @Override
            public void run() {
                // 2.显示加锁
                lock.lock();
                // 3.执行逻辑代码
                System.out.println("test");
                // 4.显示解锁
                lock.unlock();
            }
        };
        // 5.启动线程
        t.start();
    }
}

三、源码分析

3.1 创建 ReentrantLock 对象

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

首先创建 ReentrantLock 对象的时候会调用它的构造方法,通过上面的几行代码我们可以看到,默认情况下我们会调用 ReentrantLock 的无参构造器,这时创建出来的 ReentrantLock 中的 sync 属性(Sync 是ReentrantLock的一个内部类)是一个非公平同步器,而当我们选择使用有参构造器时,我们可以通过传参的方式创建一个公平同步器

对于这里的公平和非公平其实也就是我们说的锁的公平性,对于 JVM 实现的 synchronized 内置锁它是一款非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。一般来说,锁的公平性是指一个先来后到的次序,我们都知道当在多线程并发的环境下,对于锁住的资源我们会让那个抢到锁的线程来对其执行操作,而其余的没有抢到锁的线程一般都会被方法哦一个 FIFO 的队列当中进行排队,等待再次被唤醒后继续抢锁。

那么我们可以假设当一个线程刚好执行完毕后释放锁,此时一个线程进入,如果当前的锁是公平锁,那么它就一定会到上面说到的队列中排队,此时被唤醒的应当是队首的那个等待线程,换句话说也就是线程之间是平等的,大家都要排队后才能拿到锁(当队列中仍存在线程等待时),这么看来线程获取锁的操作就是公平的。相反,如果这个线程进入后没有排队,而是直接跟队首的线程来进行抢锁(抢成功了就它来执行,失败就继续排队),那么我们就说这种现象是不公平的,也就是一种非公平的锁。具体的代码实现我们在接下来阅读 AQS 中 CLH 的代码时可以遇到,这里我们接着往下看。

3.2 创建 Sync 对象

从上面 ReentrantLock 构造器的代码可以看到,当我们初始化 ReentrantLock 的时候,同时实例化了一个 Sync 对象并将其赋给了自己的属性,那么 Sync 对象是用来干什么的呢,我们直接来看代码。

3.2.1 Sync

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            // ...
        }

        protected final boolean tryRelease(int releases) {
            // ...
        }

        // ...
    }

 Sync 类是 NonfairSync 和 FairSync 的共同父类,它继承自 AQS 类,并且定义了抽象的 lock 方法。在这里我们省略了 Sync 类中的部分方法的具体实现,主要关注其中比较重要的两个方法,一个是 nonfairTryAcquire ,它是下面我们将要提到的 Sync 的一个具体实现类 NonfairSync 中 tryAcquire 实际调用的方法,而 tryRelease 这个方法正是重写了父类 AQS 中的方法。

3.2.2 NonfairSync(非公平锁)

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // 如果 CAS 操作成功说明当前线程获取到锁
            if (compareAndSetState(0, 1))
                // 保存当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 获取失败就执行与公平锁相同的操作
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            // 调用父类 Sync 中的方法
            return nonfairTryAcquire(acquires);
        }
    }


    /**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

 首先我们先来看 NonfairSync ,也就是非公平同步器。根据上面对公平锁和非公平锁含义的介绍,大家应该已经可以了解这两种锁的区别了,通过这段代码我们可以直接看到非公平锁的执行逻辑,首先就像我们前面说的,新的线程来了之后立即就会去尝试获取锁(公平锁会先排队),如果获取锁成功了,那么就直接将当前的线程绑定到这个锁的属性上(通过 setExclusiveOwnerThread 方法),如果失败了那么久乖乖的去执行跟公平锁一样的排队逻辑,那么接下来我们再来看看公平同步器的实现。

3.2.3 FairSync(公平锁)

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            // ...
        }
    }

为了和上面的非公平锁的逻辑来进行对比,我们先将 tryAcquire 方法的代码省略掉(这里要注意,非公平锁的 tryAcquire 方法实际上是调用父类 Sync 中的 nonfairTryAcquire 方法,而公平锁的 tryAcquire 方法是直接重写了父类 AQS 中的 tryAcquire 方法),在这里我们就能很清楚的看到公平锁和非公平锁代码的差异,即公平锁永远都是直接执行 acquire 添加队列的逻辑,不会上来就直接通过 CAS 来抢锁。

通过上面的分析,我们已经大致捋清了在示例代码中使用 ReentrantLock 进行显示加锁的内部代码逻辑,并且也知道了在 ReentrantLock 的构造器中会实例化一个 Sync 对象,并且这个 Sync 类就是 AQS 的一个子类,这样我们就已经把 ReentrantLock 和 AQS 连接起来了,接下来我们继续顺着代码的逻辑思路向下思考。

3.3 lock 方法

    // ReentrantLock
    public void lock() {
        sync.lock();
    }


    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
        // ...
        }
    }

首先,在示例代码中我们在使用 ReentrantLock 都会去调用它的 lock 方法进行显示的加锁,在同步代码执行结束后,再使用 unlock 进行显示解锁,因此我们接下来就进入到 ReentrantLock 的 lock 方法中。但是当我们跟进后会发现,其实 ReentrantLock 中的 lock 方法实际就是调用了我们刚刚实例化的 Sync 对象中的 lock 方法,所以我们继续跟进代码。

然后我们就会看到 不管是在 NonfairSync 还是在 FairSync 中,它们都调用了 acquire 方法,并且入参为 1,所以我们这里直接跟进 acquire 方法。

3.4 acquire 方法

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire 方法是整个加锁流程中至关重要的一个方法,它虽然只有几行代码,但是直接决定了当前线程获取锁的结果,在这方法里面主要是一个 if 判断语句表达式,表达式中的三个方法每一个的直接决定了最后的结果,因此这里我们先介绍一下这几个方法的大概作用:

(1)tryAcquire :尝试获取锁(返回值取反);

(2)addWaiter :创建一个新的 CLH 节点,将线程保存后插入队尾进行排队;

(3)acquireQueued :自旋等待直到获取锁(返回值是线程在等待过程中是否被中断过);

(4)selfInterrupt :恢复线程的中断标记(因为在 acquireQueued 中可能会被清除中断标记);

这里面需要注意的是 tryAcquire 方法,这个方法的返回值会在表达式中进行取反操作,也就是说当 tryAcquire 返回 true 时,即线程成功获取锁,不需要进行下面的 addWaiter 操作(表达式短路)。反之,如果 tryAcquire 返回 false ,那么就说明线程获取锁失败,就要执行 addWaiter 方法被添加到队列中排队。

因为这几个方法都很重要,所以我们接下来就挨个方法走进去看看。

3.5 tryAcquire(Fair 版本)

        /**
         * The synchronization state.
         * Belongs to ReentrantLock.
         */
        private volatile int state;        

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {

            // 保存当前线程
            final Thread current = Thread.currentThread();

            // 获取锁状态
            int c = getState();

            // 如果状态为未被上锁
            if (c == 0) {

                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {

                    // 将锁的拥有者设置为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }

            // 如果状态为已被上锁且锁的拥有者为当前线程(可重入)
            else if (current == getExclusiveOwnerThread()) {
                
                // 新的重入次数 = 原始重入次数 + 新增重入次数(这里 acquires = 1)
                int nextc = c + acquires;

                // 如果计算后的重入次数小于零则直接抛异常
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
        
                // 设置重新计算后的重入次数
                setState(nextc);

                return true;
            }

            return false;
        }

这部分的代码逻辑比较简单,首先是先保存当前线程,然后获取当前锁的状态,也就是 state 的值(state == 0 说明锁未被占用,state == 1 说明锁已被占用,当 state > 1 的时候说明此时的锁为重入状态)。接下来的代码执行就需要分不同的情况来看了。

首先我们假设此时锁的状态为未被占用(c == 0),那么就会进入到第一个判断体中,然后开始进行第二个判断,第二步判断中有一个 hasQueuedPredecessors 方法,它的主要作用是判断当前线程是否需要排队,因为这个方法的情况比较复杂,我们把它放到下面去单独讲,这里假设它返回了 true,也就是当前线程需要排队,那么在取反之后就变成了 false,所以结束判断直接返回 false,又因为我们在上面说过,当 tryAcquire 返回 false 的时候取反后为 true,因此就会执行 addWaiter 方法去将该线程添加到等待队列中。

然后我们再看另一种情况,假设此时锁的状态仍然为未被占用(c == 0),那么还是进入到第一个判断体中开始第二步判断,这次 hasQueuedPredecessors 方法返回 false,也就是当前线程不要排队,这样取反后为 true,代码继续执行 compareAndSetState 方法去获取锁,如果获取锁成功,那么就直接调用 setExclusiveOwnerThread 方法将锁与当前线程绑定,然后直接返回 true,这时 tryAcquire 返回值取反后为 false,根据表达式短路定理,直接结束判断,所以此时线程成功获取锁,不需要被添加到队列中。

然后,如果此时锁的状态为已被占用(c != 0),并且结束第一个判断之后第二个判断成立(持有锁的线程刚好是当前线程),那么就直接执行重入锁的逻辑,具体的逻辑代码比较简单,就不赘述了,需要注意的是这时 tryAcquire 方法返回的仍然是 true,也就是说在这种重入的状态下,线程也是成功获取到了锁,不需要被添加到等待队列中了。

最后一种情况,假如此时锁的状态为已被占用(c != 0),且持有锁的线程不是当前线程,那么两个判断语句都无法进入,则直接返回 false,然后通过 addWaiter 方法去执行添加等待队列的逻辑。

 

3.6 hasQueuedPredecessors

    /**
     * Queries whether any threads have been waiting to acquire longer
     * than the current thread.
     *
     * <p>An invocation of this method is equivalent to (but may be
     * more efficient than):
     *  <pre> {@code
     * getFirstQueuedThread() != Thread.currentThread() &&
     * hasQueuedThreads()}</pre>
     *
     * <p>Note that because cancellations due to interrupts and
     * timeouts may occur at any time, a {@code true} return does not
     * guarantee that some other thread will acquire before the current
     * thread.  Likewise, it is possible for another thread to win a
     * race to enqueue after this method has returned {@code false},
     * due to the queue being empty.
     *
     * <p>This method is designed to be used by a fair synchronizer to
     * avoid <a href="AbstractQueuedSynchronizer#barging">barging</a>.
     * Such a synchronizer's {@link #tryAcquire} method should return
     * {@code false}, and its {@link #tryAcquireShared} method should
     * return a negative value, if this method returns {@code true}
     * (unless this is a reentrant acquire).  For example, the {@code
     * tryAcquire} method for a fair, reentrant, exclusive mode
     * synchronizer might look like this:
     *
     *  <pre> {@code
     * protected boolean tryAcquire(int arg) {
     *   if (isHeldExclusively()) {
     *     // A reentrant acquire; increment hold count
     *     return true;
     *   } else if (hasQueuedPredecessors()) {
     *     return false;
     *   } else {
     *     // try to acquire normally
     *   }
     * }}</pre>
     *
     * @return {@code true} if there is a queued thread preceding the
     *         current thread, and {@code false} if the current thread
     *         is at the head of the queue or the queue is empty
     * @since 1.7
     */
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

对于 hasQueuedPredecessors 方法,与前面不同它是直接位于 AQS 基类中的方法,我这里直接把源码中整个方法加注释全部拽了出来,目的不是为了用注释来吓吓你们,而是直接的证明一下这个方法的重要性。根据注解中的描述,我们可以大概的了解这个方法的作用,就是查询是否有其它的线程已经比当前线程等待了更长的时间,而它的返回值即如果当前线程前面存在正在排队的线程时返回 true,如果当前线程位于队列的头部或者队列为空时返回 false

根据上文之前的分析,那么也就是说当当前线程前面存在排队线程时,方法返回 true,外部取反后为 false,因此 tryAcquire 方法整体返回 false,方法返回后在 acquire 方法的 if 判断语句中会再次取反,所以此时为 true,因此最后会调用 addWaiter 方法将当前线程进行排队。反之如果当前线程位于队首或者当前队列为空(或未被初始化),那么返回 false,按照上面的逻辑返回后取反为 true,因此会在 tryAcquire 方法的 if 判断语句用继续调用 CAS 来获取锁。

然后我们进入到方法体里面来对方法的细节进行分析,首先前几行代码很容易理解,就是保存等待队列的首尾节点便于后面使用,然后就是一个返回的表达式,这个表达式一共有三个小的判断,但是每一个里面都有很深的坑,所以我们一种情况一种情况的来进行分析,首先我们先分析第一个表达式 h != t 

(1)当队列未被初始化的时候,此时 h 和 t 的值应当都为 null,所以 h == t,那么此时表达式直接短路返回 false,然后按照上面分析直接去通过 CAS 去获取锁(此时没有等待队列)。我们也可以这样理解,当前没有等待队列,那么也就意味着当前还没有线程在等待,那么新线程当然可以直接去尝试获取锁。

(2)当队列被初始化且队列中的线程不大于一个的时候,此时 h 和 t 指向不同的节点(节点可以理解为对线程包装的对象),所以 h != t,然后继续向下判断 s == null(s 为首节点的下一个节点,这个表达式就是判断当前队列中的节点数),唉?这里就有问题了,这是在干嘛,为什么要判断首节点的下一个节点,而不是直接判断首节点?其实这里就有一个比较深的坑,涉及到 AQS 在设计 CLH 等待队列时的一种思想,也就是首节点永远不排队,这一点可以怎么理解呢,引用网上的一个例子,即当你在购买东西结账的时候,排在队列第一个的人应该是正在被服务的(正在结账),所以从一定意义上来说他其实不算在排队。也就是说,刚刚队列中的第一个人,并不是正在排队的第一个人,正在排队等待的第一个人应该是整个队列中的第二个人(因为此时第一个人正在结账)。

所以这样我们也就可以理解为什么它是取首节点的下一个节点了,因为下一个节点才是真正在排队的节点。而首节点应当是正在被执行的线程或者说正持有锁的线程(首节点一般是被虚拟出来线程为空或者是线程正持有锁的节点,具体原因下面分析)。然后因为我们假设当前等待队列中的线程大于一个,所以 s != null ,那么 s == null 判断结果应为 false。但是因为后面整体是一个或表达式 ,所以我们还需要进一步判断最后一个子表达式 s.thread != Thread.currentThread() ,这个表达式判断的就是当前队列中排在第二位(第一位正持有锁,第二位正在等待锁)的线程是不是当前线程,所以这里也存在两种情况。

第一种情况,假如当前排队等待第一位(队列第二位)不是当前线程,那么 s.thread != Thread.currentThread() 表达式返回 true,然后 ((s = h.next) == null || s.thread != Thread.currentThread()) 表达式整体为 true (false || true = true),那么 hasQueuedPredecessors 方法返回 true,按照之前的分析,返回到 acquire 方法后最终会调用 addWaiter 方法去将当前线程排队。

第二种情况,假如当前排队等待第一位(队列第二位)是当前线程(可重入状态),那么 s.thread != Thread.currentThread() 表达式返回 false,然后 ((s = h.next) == null || s.thread != Thread.currentThread()) 表达式整体为 false (false || false = false),那么 hasQueuedPredecessors 方法返回 false,按照之前的分析,返回到 tryAcquire 方法取反后为 true,然后就会通过 CAS 来尝试获取锁,如果获取锁成功(队首刚刚正在执行的线程刚好释放锁)那么 tryAcquire 方法整体返回 true,此时不再需要调用 addWaiter 进行排队,反之如果 CAS 获取锁失败(队首线程仍在执行),那么 tryAcquire 方法直接返回 false,之后会在 acquire 方法中调用 addWaiter 方法进行排队。

这里需要注意的是在第二种情况中,其实是一种可重入的状态,即当前排队的第一个(队列的第二个)线程刚好是当前线程,那么当该线程通过在 tryAcquire 方法中执行 hasQueuedPredecessors 方法确认可重入返回后,立即就可以通过 CAS 方法来尝试获取锁。

(3)最后我们再来分析当队列被初始化且队列中的线程仅有一个的时候,此时 h 和 t 指向相同的节点,因此存在 h == t ,所以判断语句的第一个条件 h != t 判断为 false,hasQueuedPredecessors 方法直接返回 false,然后直接在 tryAcquire 方法中通过 CAS 来尝试获取锁。

但是我们可以思考一下,到底在什么情况下,队列才会被初始化了但仅有一个线程?首先,当第一个线程到来时,因为队列还未被初始化,所以根据上面的代码我们分析它是不会进行排队的,通过接下来的代码阅读我们会发现队列的初始化是在第二个线程到来的时候完成的。当第二个线程到来时,发现队列未被初始化,但获取锁失败时,他会初始化创建一个 CLH 队列,并创建一个虚拟的节点(节点对应的线程为 Null,但其实这个节点就对应着正在执行的第一个线程)加入队列,然后自己也到队列中进行排队,所以当第二个线程到来时队列中的线程也不是为一。

此时还有一种情况,就是假如我们之前存在四个线程,在之后的运行过程中也不再有新的线程来获取锁,那么当前三个线程运行结束,第四个线程会把自己设置为首节点,并且因为它的后面不再有等待的线程节点,所以此时队列中就仅有一个线程节点,此时再有一个新的线程到来时,队列中仅有一个线程节点,所以它会直接尝试使用 CAS 获取锁,获取成功拿到锁,获取失败就去排队。

3.7 addWaiter

    /**
     * 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) {
    
        // 将当前线程封装为节点
        Node node = new Node(Thread.currentThread(), mode);

        // Try the fast path of enq; backup to full enq on failure
        // 创建新的 pred 变量保存 tail 队尾节点
        Node pred = tail;
        
        // 如果队尾节点不为空则说明队列已经被创建初始化过
        if (pred != null) {
            
            // 将当前线程节点的前置节点设为原队尾节点
            node.prev = pred;
            
            // 使用 CAS 算法将当前线程节点设置为队尾节点
            if (compareAndSetTail(pred, node)) {

                // 将原队尾节点的后置节点设置为当前线程节点
                pred.next = node;

                // 返回封装后的当前线程节点
                return node;
            }
        }

        // 创建队列并添加当前线程节点
        enq(node);

        // 返回封装后的当前线程节点
        return node;
    }

 在看完了判断语句中的第一个方法后,我们进入到下一个方法的研究,即当通过 tryAcquire 方法获取锁失败后,会调用 addWaiter 方法来将当前线程封装为节点后添加到队列中进行排队。这个方法的代码逻辑相对来说比较简单,所以大部分的执行逻辑我都直接注释在了代码中。

代码的逻辑,说白了就是现将当前线程封装到节点中,然后判断当前等待队列是否被初始化,如果已经初始化过(尾结点不为空)则直接通过 CAS 算法来将节点接到队列的尾部,然后返回当前节点。反之,如果发现等待队列尚未被初始化,则先调用 enq 方法来创建一个队列,然后再将当前的节点接到队列的尾部。

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {

        // for 死循环
        for (;;) {

            // 使用变量保存尾结点
            Node t = tail;

            // 尾结点为空时(第一次循环,队列未被初始化)
            if (t == null) { // Must initialize
                
                // CAS 算法创建并设置队首节点(首节点对应线程为 null)
                if (compareAndSetHead(new Node()))

                    // 将初始化后的队首节点赋给队尾节点
                    tail = head;

            } else {
                
                // 第二次循环时 t 不为空是首节点,将当前线程节点的前置节点设为 t
                node.prev = t;
                
                // CAS 算法将当前线程节点设置为尾结点
                if (compareAndSetTail(t, node)) {
                    
                    // 将 t 的后置节点设置为当前线程节点
                    t.next = node;

                    // 返回队列的首节点
                    return t;
                }
            }
        }
    }


    /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

对于 enq 方法的逻辑稍微有一点点复杂, 比较有趣的是它是利用了 for 的两次循环,当第一次循环时发现队列未被初始化(队尾节点为空),则走第一个判断语句块直接创建一个节点(对应当前正在执行的线程,但是该节点的线程值为 null)作为首尾节点,在第二次循环时,发现队列已经被初始化(尾结点不为空),则走第二个语句块将当前线程对应的节点接到队列的尾部(关于 CLH 队列的结构和代码这里不过多讲解,之后专门会写博文来介绍)。

3.8 acquireQueued

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {

        // 记录标志
        boolean failed = true;
        try {

            // 线程是否被中断标志
            boolean interrupted = false;

            // for 死循环
            for (;;) {
                
                // 获取当前节点的前置节点
                final Node p = node.predecessor();

                // 如果前置节点为首节点并且获取锁成功
                if (p == head && tryAcquire(arg)) {
                    
                    // 将当前线程节点设置为首节点
                    setHead(node);

                    // 将原首节点后置节点设为 null 来分离原首节点
                    p.next = null; // help GC

                    // 获取锁成功
                    failed = false;

                    // 返回线程中断标志
                    return interrupted;
                }

                // 检测当前线程是否应当被阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    
                    // 阻塞当前线程并检测当前线程是否被中断
                    parkAndCheckInterrupt())
                    interrupted = true;

            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

接下来我们来分析 acquireQueued 方法,这个方法的作用正如其注释所讲主要是用于等待获取锁,它的返回值是在等待的过程中线程是否被中断过。

首先代码中上来就是一个 for 的死循环,当线程第一次进入时,会先获取当前线程的前置节点,并判断其是否为首节点,如果当前线程的前置节点为首节点,那么就证明当前线程节点是队列中的第二个节点,也就是正在等待获取锁而排队的第一个节点,这时他会尝试使用 tryAcquire 方法再次去获取一次锁,如果获取成功那么就直接断开其前置节点,并将其设置为前置节点,然后返回中断标志。

在这里大家可能存在一个疑问,我们刚刚进入之前不是已经使用 tryAcquire 尝试获取过一次了嘛,为什么这里还要再次获取呢,我觉得可以这么理解,在高并发的情况下,如果线程任务的执行速度特别快,可能在我们刚刚执行到这一步时,恰好它已经上升到了排队的第一位,并且我们知道阻塞和唤醒线程也是存在性能消耗的,因此线程能可在 park 之前再尝试一次,看看是否能够获取到锁,这时如果能获取到锁就避免了阻塞和唤醒当前线程的性能消耗,直接让当前线程拿到锁,但如果其拿不到锁,那也不要紧,就让其继续去排队即可。

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    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;

    
    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        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;
    }


    /**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

再往下进入到了一个判断语句,这个语句中一共有两个方法,我们先看第一个 shouldParkAfterFailedAcquire ,这个方法的主要作用就像它的命名所说在获取锁失败后返回当前线程是否应当阻塞。为了便于理解,我将这个方法当中涉及的信号量 SIGNAL 的含义也贴在了上面,根据注释翻译过来,意思就是当一个节点为这个状态的时候,它的后置节点线程为待唤醒状态,需要注意的是这个信号量的值默认是为 0 的。

因此我们分析这个方法,根据前置节点信号量的指引,如果当前线程的状态为 0(初始状态),那么会进入到最后一个判断语句块中,执行 compareAndSetWaitStatus 方法,来将前置节点的状态设置为 SIGNAL ,然后返回 false,也就是说当前节点不会被阻塞,而是会再进行一轮循环(说白了这轮循环的作用就是给前置节点打上信号量)。接下来再进行循环,再次获取锁,再次失败后,再次进入到这个方法,此时前置节点的状态应为 SIGNAL ,那么就意味着它目前的状态是指明了它的后置节点是需要被唤醒的,所以这时当前节点就可以安全的阻塞了,所以此时返回 true

我觉得这里之所以进行了两次循环的判断,主要的目的是为了确保当前节点的前置节点的状态为 SIGNAL ,这样才能保证在当前节点阻塞后,其前置节点能够正常的将其唤醒。我们可以假设一下,假如当前节点的前置节点的状态为默认状态,那么当当前节点执行完毕后,可能不会去唤醒它的后置节点(因为其状态非 SIGNAL),这就会导致它的后置节点可能会一直被阻塞而无法被唤醒。


    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        // 阻塞当前线程
        LockSupport.park(this);

        // 返回线程的中断标记
        return Thread.interrupted();
    }

当 shouldParkAfterFailedAcquire 返回 true 后就表明当前节点可以安全的(能够被唤醒)阻塞了,这时就会调用 parkAndCheckInterrupt 方法来阻塞当前的线程,并返回该线程的中断标记。这里需要注意的是这点就是与正常的 CLH 队列不一样的地方,在正常的 CLH 队列中,线程是不会被阻塞的,而是会一直的进行自旋,因此我们将 AQS 中的队列成为 CLH 的变种队列

3.9 selfInterrupt

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

最后这个方法就没有太多好说的了,就是当在发现当前线程被打了中断标记后,中断当前线程。到这里为止,ReentrantLock 的主要流程源码分析就大致结束了。

 

四、内容总结

整个这篇博文大概写了两三天的时间,一边阅读代码,一边阅读资料,一边思考,一边记录,中间还经历过一次因为官方 Bug 导致中间一大部分内容丢失后又再次重写,花费了比较长的时间。本来开始的意愿是将这篇博文尽量的浓缩,文字简洁一些,但是写着写着发现很多地方的很多情况不是三言两语能解释清楚地,因此又花了很多的文字去解释他们。

因此这篇博文算是比较详细的一篇初稿吧,内容总结我会放在之后的博文当中,打算再这篇博文的基础上将 AQS 的内容再次进行一次浓缩,让文章更加简练一些。同时,后续我还会继续对 AQS 中的 CLH 变种队列和 AQS 中其他的一些分支代码进行大概两三篇博文的分析。

纸上得来终觉浅,绝知此事要躬行。

发布了244 篇原创文章 · 获赞 32 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_40697071/article/details/100854631