重入锁 ReentrantLock 源码浅析(一)

262 阅读10分钟

Java 除了使用关键字 synchronized 外,还使用了 ReentrantLock 实现独占、可重入锁的功能。相较于 synchronized,ReentrantLock 更为丰富和灵活。除此之外,我们或多或少还听说过一个名词 AQS。AQS 是Java 提供的底层同步工具,用一个 int 类型的变量表示同步状态,提供一系列的 CAS 操作来管理这个同步状态。我们今天的主角 ReentrantLock 也是基于 AQS 实现的。好的,废话少说,我们开始源码的探索路程。

构造函数

无参构造,通过无参源码可知,默认情况下是创建的非公平锁。

也就是说 ReentrantLock lock = new ReentrantLock();这是非公平的。

public ReentrantLock() {
    sync = new NonfairSync();
}

那么如何调用公平锁呢?

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

从源码得知,如果我们传了一个 true 参数,就会调用公平锁。

ReentrantLock lock = new ReentrantLock(true);这是公平锁。

属性

首先啊,我们打开源码就看到了 ReentrantLock 实现了 Lock 接口,那么 Lock 里面的一些加锁啊解锁啊等方法自然也要一一实现。

public class ReentrantLock implements Lock, java.io.Serializable {

其实ReentrantLock 就一个属性,那就是 sync。但 ReentrantLock 自己实现了一个 Sync 的静态内部类。这个 Sync 继承了 AbstractQueuedSynchronizer,也就是我们常说的 AQS。所以 AQS 中的一些重要属性自然而然也被ReentrantLock 继承。

abstract static class Sync extends AbstractQueuedSynchronizer {

进入 AbstractQueuedSynchronizer,我们依然可以看到里面也实现了一个静态内部类 Node。这个 Node 至关重要,它会出现在接下来的所有内容中。这次我只列举了ReentrantLock 需要用到的属性。

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        /** 共享模式 */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        /** 独占模式 */
        static final Node EXCLUSIVE = null;

        /** 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;
        /** node 节点的状态 */
        volatile int waitStatus;
        /** 因为需要构建双向链表,所以 prev 表示指向 node 节点的前一个节点*/
        volatile Node prev;
        /** 指向 node 节点的后继节点*/
        volatile Node next;
        /** node 封装的线程 */
        volatile Thread thread;
        /** 阻塞队列的头节点 头节点对应的都是当前线程*/
        /** 这里的 head 并不是阻塞队列双向链表的 head,可以把 head.next 当做阻塞队列的开始
         *  我在这里就犯了迷糊,在空队列的定义上犹豫了一会,所以这里给大家提个醒。 
         */
        private transient volatile Node head;

        /**
         * Tail of the wait queue, lazily initialized.  Modified only via
         * method enq to add new wait node.
         */
        /** 阻塞队列的尾节点 */
        private transient volatile Node tail;

        /**
         * The synchronization state.
         */
        // 表示资源
        // 独占模式:0 表示未加无锁状态,>0 表示加锁状态
        private volatile int state;
}

ReentrantLock 里面需要用到的属性就大致讲完了,其实还有一个状态码为 0 的,它代表当前节点是新建的节点。哦,对了,AbstractQueuedSynchronizer 还继承了一个抽象类 AbstractOwnableSynchronizer,里面的exclusiveOwnerThread 属性及其 get、set 方法也是重点对象。

// 独占锁模式:表示当前独占锁线程
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
     exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
     return exclusiveOwnerThread;
}

介绍完属性,接下来开始进入源码的阅读,这次主讲公平锁。先大致列举一下加锁阶段的方法。

lock()

      |–>acquire() 竞争资源

                  |–> tryAquire() 尝试获取锁

                              |–> acquireQueued() 挂起当前线程

                              |–> shouldParkAfterFailedAcquire()// 当前线程获取锁资源失败后是否需要挂起呢?

                              true: 需要; false 不需要

                                            |–> parkAndCheckInterrupt() 挂起当前线程,唤醒之后返回当前线程的中断标记

                              | –> addWaiter()将当前线程封装成node

OK,大致骨架梳理完了,源码开始,gogogogo!!

acquire(int arg) – 加锁

// 公平锁入口,不响应中断的加锁
final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
    // 尝试获取锁,成功返回 true,失败返回 false.
    /* addWaiter 将当前线程封装成 node 入队
     * acquireQueued 挂起当前线程 
     *               返回 true 表示挂起过程中线程被中断唤醒过
     *                                返回 false 表示未被中断过
     */

    if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

其中,acquire() 就是竞争资源的方法,首先回去尝试获取锁,成功了就执行其业务逻辑,不成功会将其包装成Node进入阻塞队列中并挂起。

tryAcquire(int acquires) – 尝试获取锁

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // AQS state
    int c = getState();
    // state == 0 表示当前 AQS 处于无锁状态
    if (c == 0) {
        /**
         * 条件1:hasQueuedPredecessors() 
         *       true:当前线程前面有等待者,当前线程需要入队等待
         *       false:当前线程前面无等待者,直接尝试获取锁
         * 条件2:通过 CAS 的方式设置 state
         *       success:当前线程抢占锁成功
         *       fail:存在竞争,且当前线程竞争失败
         */
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //设置当前线程为独占者 线程
            // 注意:这里没有设置阻塞队列的 head 节点,后续在分析 addWaiter 的 enq 会提到
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // c != 0 需要检查当前线程是不是 独占锁的线程,因为ReentrantLock 是可重入的
    else if (current == getExclusiveOwnerThread()) {
        // 更新值
        int nextc = c + acquires;
        // 越界判断
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 1.CAS 失败  c == 0 时,CAS 修改 sate时未抢过其他线程
    // 2. c > 0 且 ownerThread != currentThread.
    return false;
}

回到 acquire方法,tryAcquire 如果返回 true 取反后为 false,就返回业务层面了。可如果tryAcquire 为 false,取反后为 true,那么就要进入阻塞队列挂起了。接下来我们看一下 addWaiter()。

入队

private Node addWaiter(Node mode) {
    // 将当前节点封装到 node 中
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 快速入队
    // 条件成立:表示队列里面有 node
    if (pred != null) {
        // 将node 的 prev 指向 pred
        node.prev = pred;
        // 通过 cas 的方式将 node 入队
        if (compareAndSetTail(pred, node)) {
            // 前置节点指向 node,完成双向绑定。
            pred.next = node;
            return node;
        }
    }
    // 什么时候会执行到这里?
    //1. 当前队列是空队列 tail == null
    //2. cas 竞争入队失败

    // 完整入队
    enq(node);
    return node;
}

其实,addWaiter 方法无非就是将当前线程封装成 node节点加入到阻塞队列队尾中。如果一切进展顺利即上面所说的“快速入队”。如果中间出现阻塞队列为空队列或者 cas 插入队尾失败的情况则进入完整入队。接下来我们分析一下完整入队 enq()。

private Node enq(final Node node) {
    /** 自旋入队,只有当前 node 入队成功后,才会跳出循环*/
    for (;;) {
        Node t = tail;
        /*
         * 队列为空队列
         * 说明当前锁 被占用,且当前线程有可能是第一个获取锁失败的线程
         * (为什么是有可能?因为当前时刻可能存在一批获取锁失败的线程.)
         */ 
        if (t == null) { // Must initialize
            // 作为当前持锁线程的第一个后继线程
            // 1.因为当前持锁线程在获取锁时(tryAcquire),成功了,并没有向阻塞队列中添加
            //   任何元素,所以这里要通过 cas 方式给阻塞队列添加头节点。
            // 2.自己追加节点
            // 然后我们便开始进入下一轮循环,下一轮直接进入 else
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 由于阻塞队列中已经有我们之前创建的节点了,所以我们直接将node的 prev指向 tail
            node.prev = t;
            // 通过cas的方式将当前 node 设置成新的尾节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

竞争逻辑

shouldParkAfterFailedAcquire 当前线程获取锁资源失败后是否需要挂起呢?true:需要挂起;false:不需要。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前置节点的状态 (前面有写,建议翻看)
    int ws = pred.waitStatus;
    // 成立:前置节点是可以唤醒当前节点的,返回 true 后,parkAndCheckInterrupt park当前线程
    // 普通情况下,第一次来到 该方法 ws 不会是 -1
    if (ws == Node.SIGNAL)
        return true;
    // ws > 0 表示前置节点是 CANCELED 状态
    if (ws > 0) {
        // 唤醒 canceled 节点的条件是当前节点的前置节点的 ws 为-1
        // 那么这个 do while 循环就不断地向前找状态不大于 0 的节点
        // 那么 ws > 0 的节点会出队(下面有图,可以参考一下)
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果当前 node 前置节点的状态为 0,
        // 则会将当前线程 node 前置节点状态强制设置为 SINGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

下图是找唤醒节点的过程。(太丑了,没办法)

image.png

总结:
        当前节点的前置节点是取消状态,第一次来到这个方法时会越过取消状态的节点,第二次会返回 true,然后 park 当前线程。 当前节点的前置节点状态是 0,当前线程会设置前置节点的状态为 -1,第二次自旋来到这个方法时,会返回true 然后 Park 当前线程。 接下来是 parkAndCheckInterrupt()

它是 AQS 的方法,park 当前线程 将当前线程挂起,唤醒后返回当前线程是否为中断信号唤醒。

private final boolean parkAndCheckInterrupt() {
  LockSupport.park(this);
  return Thread.interrupted();
}

加锁总结

以上就是这篇文章的全部内容了,最后在概述一遍做个总结吧,主要是想梳理一下所有的源码知识点,毕竟无论是面试还是与人讨论我们都要大致说一下,总不能上来就滴水不漏地将整体细节全部描述出来。

  1. 如果我们用的是公平锁,那么我们就要调用FairSync 的 lock() 。
  2. 然后 lock 调用 acquire,参数传入 1,设置 AQS的 state 的值。
  3. 进入 acquire 我们首先要尝试拿到锁(tryAcquire),怎么去尝试拿到锁呢?我们先是获取当前线程,通过当前线程获取当前线程的状态值(state),如果状态值为 0,说明现在是无锁状态。然后判断当前线程前面有没有线程在等待(hasQueuedPredecessors),如果没有那么就通过 cas 的方式去设置 state值(compareAndSetState)。如果设置 state 值成功了,说明我们抢占锁成功了。把当前线程设置成独占者线程(setExclusiveOwnerThread),然后就返回 true 了。如果锁重入了呢,更新 state 值,再次设置新的 state,最后返回 true;
  4. 如果没有拿到锁,那么我们会将当前线程封装成一个 Node 节点,然后将这个节点添加到阻塞队列中。
  5. 进入到阻塞队列中我们还要看看当前线程是否是 park 状态,如果不是那么就将其挂起。并且唤醒线程的逻辑也在这个方法里。但唤醒的逻辑要下次再讲。到此整个加锁的过程全部完成。