“码”上看看ReentrantLock加锁、解锁原理

写在开始

ReentrantLock 是 java 1.5 以后新加入的除 synchronized 关键字以外的新的加锁方式,从其命名上就可以知道它也是用来加可重入锁。在使用层面上它具备了synchronized所具有的所有功能,并且由于它支持响应中断、超时等操作,所以在某些场景下它会比synchronized更加灵活。

ReentrantLock和synchronized在使用性能上是否有太大的差距呢?关于这个问题,由于我本人没有亲自实验过,所以不得而知。

但是,我估计二者应该没太大差距,随着jdk版本的不断迭代,synchronized只要用的好,基本没有性能上的瓶颈问题。而且最主要的是,jdk推出ReentrantLock并不是为了取代synchronized,而是为了弥补synchronized在某些使用场景上的不足。

在实现原理上,ReentrantLock基于AQS,而synchronized是监视器。关于这两个之间的区别,大家可以在网上搜一些相关文章看一下。如果不清楚它们也别慌,只要知道AQS是用java写出来的,监视器是JVM实现的就行。

所以呢,既然AQS是java代码写出来的(也就是人眼能看懂喽),那么,这篇文章就通过研究ReentrantLock加锁、解锁的源码,顺带来看一下AQS底层是怎么来实现支持的。

简述AQS

AQS是AbstractQueuedSynchronizer这个java类的首字母简称。这个类几乎是juc下所有并发工具的基础。

它的核心思想是,内部维护一个共享变量(state)和双端队列(GLH队列)。各个工作线程在加锁时会去争夺获取该变量,当该变量被某个线程占用时该变量的值就会被修改(初始时state=0,修改后state>0),则表示该线程现在可以执行,未获取到该变量的线程将会进入到双向链表中排队等待(同时线程本身被阻塞),而后待共享变量被释放后(state=0),队列中被阻塞的线程会依次被唤醒执行。

AQS思想简图.png

ReentrantLock 使用

我们先来看下ReentrantLock在日常中的使用方式。

public class ReentrantLockTest {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        try{
            lock.lock(); //加锁
            //搞点事情。。。
        }finally {
            lock.unlock();//解锁
        }
    }
}

上述示例,简单展示了ReentrantLock加锁、解锁的过程(记住一定用完要unlock,这一点确实不如synchronized)。当然它还支持更加多样的锁使用方式,如公平锁、非公平锁、超时加锁等等。

好了,简单看过后,接下来就点进去看下实现原理吧。

看源码

加锁过程

我们顺着上述示例中的lock方法进去,进入到ReentrantLock.java文件中

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

其内部调用了ReentrantLock类成员变量sync的lock方法。sync何许人也?下面是它在ReentrantLock类中第一次出现的地方。

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync 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();

OK,现在明确了,这玩意原来是个抽象类,然后那个lock方法就是它的抽象方法,于是我们继续找到实现这个抽象类的具体类。

    //ReentrantLock.java
     static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
    //......省略很多行
   
 //ReentrantLock.java
    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() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
  //......省略很多行

看到这些内部类的名字,此时或许你已经想到了,这应该就是实现公平锁和非公平锁的基础,我们接着在找下这两个类被实例化的地方。

// ReentrantLock.java
 /**
     * 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();
    } 

好了,确认无疑了。原来new ReentrantLock创建公平锁和公平锁时传递的那个boolean变量的背后就是在实例化不同的Sync实例。

现在在公平锁和非公平锁之间,我们选择公平锁来进行接下来的“细细品味”。因为理解了公平锁的实现原理,再回过来看非公平锁的实现过程就一目了然了(这个世界本身对于公平规则的制定就是一件非常难的事情)。

下面我们看下FairSync#lock的实现方式。

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

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

可以看到,它实质调用了一个acquire方法,继续跟进这个方法。

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

我们发现,acquire来自AbstractQueuedSynchronizer类(之后简称AQS类),而这个类就是AQS的由来和核心,也就是现在我们现在即将要探究AQS的实现原理了。

从表面看acquire方法貌似没有太多复杂的实现逻辑,下面我们分别看下其内部调用的这些方法的实现细节。

tryAcquire()

// AbstractQueuedSynchronizer.java
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

进去发现,AQS类并没有对其进行具体实现,于是开始寻找它的子类。我们发现FairSync和NonfairSync是AQS的子类,并对tryAcquire方法进行了具体的实现。

// ReentrantLock.java 
// FairSync#tryAcquire
      protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取当前state状态
            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;
        }

上述源码展示了FairSync内部类tryAcquire的具体实现。这里首先说明几个关键方法:

  1. getState():在AQS类中有一个成员变量state(如下所示),用于标识共享资源(见简述AQS)。该方法用于获取当前共享资源的值。
 // AbstractQueuedSynchronizer.java
    /**
     * The synchronization state.
     */
    private volatile int state;
  1. setState():同1,该方法用于设置共享资源state的值
  2. setExclusiveOwnerThread():在AQS父类AbstractOwnableSynchronizer中有一个成员变量(如下所示)用于标识当前独占线程,独占线程即表示当前获得state资源且正在运行的线程。该方法用于将参数中传入的线程标识为独占线程。
// AbstractOwnableSynchronizer.java
  /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;
  1. getExclusiveOwnerThread():同2,该方法用于获取当前的独占线程。

  2. hasQueuedPredecessors():判断当前CLH队列中是否还有等待中的线程(关于CLH队列,见简述AQS)。

  3. compareAndSetState():通过CAS方式设置共享资源state的值。

下面我们先看下hasQueuedPredecessors()方法的内部实现:

  // AbstractQueuedSynchronizer.java
  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());
    }

该方法由AQS类提供具体实现,内部的执行逻辑主要用于对CLH队列中的节点进行判断。其具体的判断逻辑是:

  1. 若头结点等于尾节点(h==t)返回false。此时表示队列为空,即没有正在执行中的线程且前序没有等待中的线程。
  2. 当头结点不等于尾节点(h!=t)时,若如下两个条件任何一个满足,则返回true,表示当前队列中存在排队的线程节点。
    a. 若头节点的next节点为空((s = h.next) == null)。
    b. 头节点的next所对应的线程不是当前线程(thread != Thread.currentThread())。

注意: 此时你可能会有疑问,为什么“头节点的next节点为空”也标识队列中存在排队的节点呢?

之所以有这样的考虑的原因主要有两点:

  1. CLH队列的头结点是一个虚拟节点,它的作用只是用来占位,并不代表正真意义上的线程节点,因此队列中的第二的节点才是真正的线程节点。
  2. 当前线程(A线程)执行到上述方法逻辑时,正好有另一个线程(B线程)比当前线程执行要快,且执行“入队”的操作,但是由于是双向队列,因此入队需要两步骤的操作,即第一步,B线程节点的pre指向head节点;第二步,head节点的next指向B线程节点。但是此时B线程只执行到第一步,第二步还没有执行,那么这个时候就出现了h.next==null的现象了。


    image.png

下面梳理一下tryAcquire方法的执行流程

tryAquire执行流程

从以上流程图上看,同一个线程在已经是独占线程的情况下,可以重复加锁,这也就是所谓重入锁的来源。

addWaiter()

下面我们接着看下AbstractQueuedSynchronizer#acquire方法逻辑中的addWaiter方法内部。

// AbstractQueuedSynchronizer.java
 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

这个方法也是AQS类提供的方法,从方法逻辑上大体可以看出,它是在进行一些队列操作。关于Node对象在此需要简单说明一下:

  • 我们之前一直在提的CLH队列中的每个节点实质是一个Node对象,该类中封装了前、后指针,当前线程、节点状态等信息。

我们先看一下上述方法中除enq()方法之外的逻辑:

  1. 首先new了一个关于当前线程的Node对象
  2. 然后将CLH队列的尾节点赋值给pre,并判断pre是否为空
  3. 如果pre!=null则表示当前的CLH是一个非空队列,此时对第1步创建的node对象进行入队操作。如果入队成功则返回当前node,如果失败则调用enq()方法
  4. 如果pre==null,则表示当前CLH队列为空,此时调用enq()方法

下面看一下enq()方法的内部逻辑:

// AbstractQueuedSynchronizer.java
 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

同样enq()执行逻辑也由AQS类自己提供。方法开头便是一个由for死循环构成的一个自旋操作,而后的执行逻辑如下:

  1. 判断当前队列的tail节点是否为null,如果为空的话在队列中设置一个新new的Node节点作为队列的head节点,同时tail也指向新创建的head节点(注意:之前提到的,CLH的头结点是一个虚节点,它不包含任何线程,只用于占位)。
  2. 如果tail节点不为空,则将参数传入的node节点(即addWaiter方法中创建的封装了当前线程的Node对象)入队到CLH队列中。
  3. 由于enq方法内部是一个自旋操作,所以最终都会走到第2步逻辑中。

通过上述的分析,我们不难发现enq方法本质是通过自旋转的方式将封装了当前线程的node加入的CLH队列中。

现在我们来梳理一下整个addWaiter方法的执行逻辑:


addwaiter方法执行流程

acquireQueued()

接下来我们接着看下AbstractQueuedSynchronizer#acquire方法所调用的acquireQueued()方法的执行逻辑。

// AbstractQueuedSynchronizer.java
 /**
     * 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 (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法逻辑中相信首先映入第一眼帘的,相信应该是这个标志性的for循环吧。没错,又是一个自旋操作。我们先描述一下方法里面的逻辑:

  1. 方法中有两个局部变量failed用于表示线程加锁失败,interrupted用于表示线程是否中断
  2. 方法的第一个参数Node是刚才addWaiter()方法中封装好,且加入到等待队列中的包含当前线程的Node
  3. 方法进入自旋逻辑后:
    1. 获取当前线程node节点的前驱节点(node.predecessor()),判断它 是否为head节点,如果是的话尝试去获取锁(tryAcquire()),获取成功后,将当前node设置为head节点(setHead()还记得之前说CLH队列中的头结点其实是一个占位节点吗,因为头节点中不包含任何线程,此处也一样,看下setHead方法中的具体逻辑就一目了然了),然后返回当前线程是否中断(return interrupted)
    2. 如果当前线程的前驱节点不是head节点,则调用shouldParkAfterFailedAcquire()、parkAndCheckInterrupt()方法。

下面我们分别进入shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()方法内部看下里面的逻辑。

  1. shouldParkAfterFailedAcquire()

    从该方法名字面上理解,这个方法的作用似乎是返回一个boolean值,该值表示获取锁(acquire)失败(failed)后(after),是否应该中断(park)当前线程。下面看下代码:

    // AbstractQueuedSynchronizer.java
    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;
    }
    

    纵观方法逻辑,主要是对Node中waitStatus字段值进行判断,然后对队列中节点进行相应操作。关于waitStatus值的的具体含义,大家可以查看下Node类中具体的定义的静态常量,结合上述方法逻辑这里简单描述下:当waitStatus>0时,表示对应的Node时cancelled状态;当waitStatus=-1时,表示需要将该节点(waitStatus=-1)的后继节点中的线程中断(park),即也侧面反应了该节点线程处于待唤醒执行。

    那么我们来描述下shouldParkAfterFailedAcquire()方法中的具体逻辑:
    1. 获取pre节点(当前线程节点的前序节点)waitStatus值
    2. 若该值等于-1,表示需要将当前线程中断(park),因此返回true
    3. 若该值大于0,表示pre节点处于cancelled状态,从当前线程节点开始,依次向前将所有的waitStatus>0的节点从CLH中移除。
    4. 如果不符合2、3步骤的条件,则将pre节点的waitStatus通过CAS的方式设值为-1

    因此通过上述的逻辑描述,也确实印证了该方法的目的确实返回当前线程是否需要中断,此外其中还加了一些逻辑用来剔除掉CLH队列中处于取消状态的节点

  2. parkAndCheckInterrupt()

    同上,我们从字面意思上理解,该方法用于中断(park)当前线程,并且检查(check)线程的中断状态(interrupt)。

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

    从方法逻辑上看很简单,调用了LockSupport.park方法,而后返回当前线程的中断状态。

    注意,当方法执行到LockSupport.park(this)这一步时,整个当前线程就阻塞了,也就是需要等待前序节点来唤醒它,然后才能继续执行Thread.interrupted()的逻辑,以及acquireQueued()和acquire()剩余的逻辑。

    也就是当该线程被唤醒以后,会继续将parkAndCheckInterrupt()方法执行完,在acquireQueued()方法中根据parkAndCheckInterrupt()返回结果对interrupted进行设值,而后继续进入到自旋逻辑中去获取锁资源。

下面我们再回到acquireQueued()方法中(再贴一次代码)。

// AbstractQueuedSynchronizer.java
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(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法的最后有一个finally逻辑,也就是acquireQueued()方法返回前一定会执行的一个逻辑。当failed=true的时候,会调用cancelAcquire()方法。

从方法上看failed在声明时的默认值是true,当前线程节点获取锁成功后,该值就变为false,那么也就是只有在当前线程获取锁失败的情况该值才会true,即当前线程节点获取锁失败的情况下会触发cancelAcquire()方法逻辑

那么,我们再探究下这个方法里的执行细节:

// AbstractQueuedSynchronizer.java
private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

这里我们文字描述下,这个方法的执行逻辑:

  1. 若当前节点为空,则直接return

  2. 将当前节点的Thead属性设置为null

  3. 声明一个pre变量,值为当前节点的前序节点

  4. 从当前节点开始,一直向前寻找到一个waitStatus>0(非取消)的前序节点(和shouldParkAfterFailedAcquire中的逻辑非常相似),当然寻找的过程中也操作了队列,使得当前节点的前序节点(node.prev)和pre变量最终指向了它前面的非取消节点


    image.png
  5. 将当前节点的waiStatus设值为取消状态(node.waitStatus = Node.CANCELLED)

  6. 如果当前节点是CLH队列的尾节点,那么就将它的pre节点(3步骤寻找到的)设置为CLH的尾节点,并将队列尾节点的next指向null,如下图所示:


    image.png
  7. 如果当前节点的前序节点(之后简称pre)不是头结点(即当前节点处于CLH中间),且pre的waitStatus=-1(Node.SIGNAL),或pre.waitStatus<0则将其设置为-1(Node.SIGNAL),并且pre节点中的线程不为空,那么将pre的next指向当前节点的next


    image.png
  8. 如果当前节点是头节点,那么唤醒它的后继节点线程(因为本应该是唤醒当前节点线程,但是失败了,所以让后继节点继续执行)。


    image.png

综上cancelAcquire方法的执行逻辑,我们也梳理结束了,整体看着较为复杂,其实无非就是一些常规的队列操作,看到了无需紧张。

那么,上述我们分别讲述了AbstractQueuedSynchronizer#acquireQueued()方法内部的执行细节,由于走的很深,下面我们来梳理一下整个acquireQueued()方法的执行流程:


image.png

我们再来回头看AbstractQueuedSynchronizer#acquire,先回顾一下整个加锁的执行链路

    以公平锁为例:
    ReentrantLock#lock --> FairSync#lock --> AbstractQueuedSynchronizer#acquire

再贴一下acquire()方法的代码:

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

可以看到,整个acquire加锁的逻辑就是,先通过tryAcquire尝试去获取锁,若失败,则进入CLH队列并阻塞当前线程,等待被唤醒,此时acquire方法的执行也就被hang住了。当线程被前序节点唤醒后acquire方法的逻辑继续执行(并根据acquireQueued方法返回结果来确定是否调用selfInterrupt()从而设置当前线程的中断状态),而后继续执行线程的处理逻辑。

上述就是整个公平锁的加锁过程,关于非公平锁的加锁过程和公平锁基本相似,只是在加锁时没有判断当前队列是否有前序节点,感兴趣的可以自行查看一下。

解锁过程

下面我们再来看下ReentrantLock的解锁过程,非公平锁和公平锁的公用了一套解锁逻辑。我们先简单梳理一下解锁过程中的方法调用链路:

解锁过程
ReentrantLock#unlock ---> AbstractQueuedSynchronizer#release ---> Sync#tryRelease

整个解锁过程没有加锁的过程那么复杂,我们直接先看下AbstractQueuedSynchronizer#release方法

// AbstractQueuedSynchronizer.java
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

方法逻辑很清晰:

  1. 首先调用Sync#tryRelease释放锁资源
  2. 释放成功后,将CLH队列的头结点取出,并判断头结点不为空,且非取消状态(h.waitStatus != 0),而后唤醒头结点的后继节点(unparkSuccessor)。

先来看下Sync#tryRelease方法源码:

    protected final boolean tryRelease(int releases) {
            //获取当前共享资源的值,并与releases相减
            int c = getState() - releases;
            //判断当前线程是否是当前的独占线程,若不是则直接抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果当前共享资源值和release相减后等于0,
            //则表示资源可被释放,并将当前的独占线程设置为空,
            //并将方法返回值设为true
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //设置state的值
            setState(c);
            return free;
        }

整个的执行逻辑也不复杂,具体的逻辑我在上方中加了注释。总之,当该方法返回true则表示当前共享资源无任何线程占有,即,此时可以唤醒下一个队列中线程。

我们再来看下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.
         */
        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);
    }

该方法由于加了一些队列的操作,所以可能看上去有些复杂。下面我们来梳理一下:

  1. 此时方法中传入的node节点是CLH的头结点(之后都称node为头结点)。
  2. 获取头结点的waitStatus,若小于0,则将其设为0(0可以理解为是一个无用节点)
  3. 将头结点的next节点赋值给s
  4. 若s等于空,或者s.waitStatus>0(表示s节点处于cancelled状态),则从队列尾部开始向前一直寻找到头节点之后第一个待唤醒节点(即waitStatus <= 0的节点),并将该节点赋值给s。
  5. s不为空,则将s对应的节点的线程唤醒

综上就是整个ReentrantLock的解锁过程,以及其中的执行细节。整个解锁过程还是比较直观易懂的。

写在最后

至此,我们已经将整个ReentrantLock的加锁解锁过程,以及背后的AQS是如何对其提供支持的,讲述完毕了。

整个过程如果要总结几个关键点的话,我觉得是“队列”、“CAS”、“自旋”、“共享资源state”。纵观其源码,由于涉及到一些队列操作,可能相对来说略为难读,但是经过梳理以后,其实现方式还是比较清晰的。希望通过理解源码,能够帮助我们在今后开发过程中更好的使用它。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 雨后晴空照,略感凉意浓; 已是入秋季,立马中元节。 完学已两月,闲做家中人。 堂堂七尺男,何时立顶天?
    普安阅读 79评论 0 0
  • 世界的改变,来自关系的革命 关系的革命,来自个体的觉醒 你与他人的关系,就是你与世界的关系 真正成熟的关系,应该是...
    菲儿912阅读 165评论 0 1
  • 今天我深刻领悟到:晒车!晒房!晒吃饭!那都是毛毛雨。有本事中午出来晒太阳啊!晒糊你!打败你的不是天真,是天真热!到...
    刘莉莉_feb0阅读 192评论 0 2
  • 文|恭老西 近期,有很多家长在育儿公开课群提出同样的问题,吐槽孩子很难带。其实,不难想象,现在很多家庭都是几个大人...
    恭老西有话说阅读 339评论 1 0