从源码分析ReentrantLock基本原理

从源码分析ReentrantLock基本原理

记录并发编程学习中,关于ReentrantLock可重入锁的加锁和释放锁过程。分析加锁和释放锁代码逻辑,了解其基本实现原理,对于分析过程中的理解误点,麻烦不吝赐教。

本次分析代码版本jdk1.8。

ReentrantLock基本介绍

在实际的开发场景中,synchronized的加锁场景存在不够灵活的局限性,因此java提供了Lock接口。ReentrantLock是对Lock接口的实现,synchronized和ReentrantLock都是可重入锁的实现。

AbstractQueuedSynchronizer分析

理解ReentrantLock的实现原理需要先去了解AbstractQueuedSynchronizer(后续统一称为AQS)的几个基本概念和其维护的双向链表数据结构。AQS是java实现同步的工具也是Lock用来实现线程同步的核心,在AQS中提供两种模型,一个是独占锁,一个是共享锁,本次分析的ReentrantLock就是独占锁的实现。

独占锁、共享锁

独占锁可以简单理解为,每次对于加了锁的代码,同时只能有一个线程访问(同时只有一个线程持有锁资源),而共享锁则是允许多个线程。

锁资源同步状态
/**
 * The synchronization state.
 */
private volatile int state;

state这个变量,可以理解是对锁资源的占用标识,当锁资源没有线程占用的情况下,state为0的情况标识当前没有线程持有锁资源。

双向链表

AQS底层维护一个FIFO的双向链表,链表由AQS实现内部类封装为一个Node对象,这样设计方便快速定义链表的首尾节点。链表定义见如下源码。

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
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;
节点

上面可以看到,双向链表是由node对象的封装。在node对象,主要是链表的数据结构指向链的prev和next,并封装了线程对象在node中。另外一个需要关注的点是node中的成员变量waitStatus,这个状态标识node的状态,用于后续AQS对链表的维护。了解了AQS中的几个基本概念,现在开始分析ReentrantLock的加锁过程。

ReentrantLock加锁过程分析

调用ReentrantLock加锁方法lock(),可以观察到是调用sync.lock(),而sync是继承自AQS的抽象类,sync的实现类有两个,FairSync(公平锁)和NonfairSync(非公平锁)。对于公平锁和非公平锁可以理解为,公平锁是FIFO,先来的线程会优先获得锁,具体过程见如下源码。

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

//NonfairSync
final void lock() {
    		//这段可以理解为,非公平锁的实现会在程序调用lock加锁代码的时候,以插队的形式先尝试去抢一下锁资源
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
}

核心的实现acquire(1)方法是在AQS中的实现。

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

可以看到程序先调用了tryAcquire,而在AQS中,tryAcquire只是抛出了一个异常,从官方的说明理解是设计者认为不支持排他锁的实现无需调用这个方法。因此回到tryAcquire在ReentrantLock使用时的具体实现,是由FairSync和NonfairSync来实现的。

/**
 * 公平锁实现
 * 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()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

/**
 * 非公平锁实现
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    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;
}

这里也可以看到,对于公平锁和非公平锁,会有一个排队获取的问题,在公平锁中,通过hasQueuedPredecessors方法判断当前线程在AQS中的时间顺序来确认是否允许去占锁资源。而非公平锁还是直接插队抢占。这里需要注意state这个变量,state为0时表示当前没有线程占用锁,而大于0时表示锁占用次数(此处大于0的情况为重入锁的实现,线程重入时,state会+1)。如果cas成功,才会将当前线程标识为锁的持有线程。

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

回到acquire方法,在尝试获得锁失败后,会去调用addWaiter方法,将线程加入链表。接下来分析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 node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //链表尾部节点不为空的情况下,将node设置为尾部节点,将之前尾部节点的next指向当前node
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //enq理解类似初始化的实现,对AQS链表做初始化并设置当前node为尾部节点
    enq(node);
    return node;
}

addWaiter中的实现,首先会将当前线程封装为node节点,然后将node节点设置为AQS中的尾部节点。完成后调用acquireQueued方法,接下来分析下acquireQueued的源码。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
                //获取上一个节点
                final Node p = node.predecessor();
                //如果上个节点为head节点,尝试获取锁成功。
                if (p == head && tryAcquire(arg)) {
                    //设置当前节点为head节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //这里是判断节点状态并从链表中移除无效节点,完成后,挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

在acquireQueued中,通过自旋的方式去校验当前节点是否处于就绪状态,也就是当前节点的上一个节点为head,并且通过tryAcquire方法成功占用锁,这时表名head节点执行完成并已经释放锁资源,然后将当前节点设置为head节点。如果当前节点的上一个节点不为head说明在当前节点之前还有入队时间更早的节点,需要排队。因此进入shouldParkAfterFailedAcquire方法来检查节点状态并通过unsafe包内提供的native方法来挂起线程。可以分析一下两个方法的代码。

/**
 * 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;
    //如果是SIGNAL状态,标识上一个节点状态有效,会返回true,然后让当前节点去挂起排队
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    //如果状态>0,表示上一个节点等待超时或者被中断过了,因此会从链表中移除上一个节点并循环往上检查节点有效性
    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.
         */
        //将上一个节点的状态置为SIGNAL,标识有效
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire方法用于判断当前pred节点的状态,如果pred节点状态为CANCELLED(1),则会从AQS队列中移除并将当前node指向pred节点的prev节点。如果上一个节点状态有效,则进入parkAndCheckInterrupt()方法。

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

parkAndCheckInterrupt()用于挂起当前线程。而挂起后返回线程的中断状态,这里可以理解为,因为park挂起后线程处于阻塞状态,而在阻塞状态下要么是被被unpark唤醒或者是被中断唤醒,因此去获取中断标识用于判断。这块的理解可以单独阅读关于park和interrupted的基本原理。如果返回标识为中断,后续会走selfInterrupt()方法。

附上加锁逻辑时序图。

ReentrantLock工作原理时序图(获得锁)

ReentrantLock释放锁过程分析

释放锁的流程相对于加锁简单一些,公平锁和非公平锁的释放锁流程统一有AQS实现,接下来开始分析锁的释放代码和过程。可以先看到AQS中的release方法。

/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #tryRelease}
 */
public final boolean release(int arg) {
    //释放锁资源
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒排队中的head的next节点。
            unparkSuccessor(h);
        return true;
    }
    return false;
}

在release方法中可以看到,主要的逻辑在于tryRelease和unparkSuccessor中。可以先看一下tryRelease方法。

protected final boolean tryRelease(int releases) {
    //锁资源占位标识-1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //如果资源释放完成返回true,并且释放线程占用标识
        free = true;
        setExclusiveOwnerThread(null);
    }
    //重新设置锁资源占位标识
    setState(c);
    return free;
}

可以看到在ReentrantLock中的tryRelease实现,首先会去对state - 1,这里是重入锁的设计,多次重入的情况下需要对应多次的锁资源释放,在全部释放完成后state = 0,然后设置当前独占锁的占用线程为null(也是释放资源),并重新设置锁的资源站位标识为0并返回true。而当返回true之后,开始去唤醒链表中排队的节点了。可以看到unparkSuccessor方法。

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
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.
     */
    int ws = node.waitStatus;
    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;
    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);
}

这个方法最主要的功能也就是最后这句LockSupport.unpark了。唤醒的节点是当前节点的next节点,在上面会对next做一次有效性扫描,waitStatus >0的情况上面提到过是属于CANCELLED状态。那么,当next不为CANCELLED后,就开始唤醒节点线程了。这个时候锁的释放就完成了,那么可以回过去看一下acquireQueued方法,之前被阻塞的线程会被重新唤醒,然后再次去对锁资源竞争。

附上释放锁的逻辑时序图。

ReentrantLock工作原理时序图(释放锁)

posted @ 2022-03-03 15:13  生如梦境  阅读(29)  评论(0编辑  收藏  举报