• 重磅出击,20张图带你彻底了解ReentrantLock加锁解锁的原理


    最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock加锁解锁的全过程。系好安全带,发车了。

    简单使用

    在聊它的源码之前,我们先来做个简单的使用说明。当我在IDEA中创建了一个简单的Demo之后,它会给出以下提示

    提示文字

    在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

    • 1、如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。

    • 2、如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQStryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。

    • 3、在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。

    java.concurrent.LockShouldWithTryFinallyRule.rule.desc

    还举了两个例子,正确案例如下:

    1. Lock lock = new XxxLock();
    2. // ...
    3. lock.lock();
    4. try {
    5.     doSomething();
    6.     doOthers();
    7. finally {
    8.     lock.unlock();
    9. }

    错误案例如下:

    1. Lock lock = new XxxLock();
    2. // ...
    3. try {
    4.     // 如果在此抛出异常,会直接执行 finally 块的代码
    5.     doSomething();
    6.     // 不管锁是否成功,finally 块都会执行
    7.     lock.lock();
    8.     doOthers();
    9. finally {
    10.     lock.unlock();

    AQS

    上边的案例中加锁调用的是lock()方法,解锁用的是unlock()方法,而通过查看源码发现它们都是调用的内部静态抽象类Sync的相关方法。

    abstract static class Sync extends AbstractQueuedSynchronizer

    Sync 是通过继承AbstractQueuedSynchronizer来实现的,没错,AbstractQueuedSynchronizer就是AQS的全称。AQS内部维护着一个FIFO的双向队列(CLH),ReentrantLock也是基于它来实现的,先来张图感受下。

    Node 属性

    1. //此处是 Node 的部分属性
    2. static final class Node {
    3.  
    4.  //排他锁标识
    5.  static final Node EXCLUSIVE = null;
    6.  //如果带有这个标识,证明是失效了
    7.  static final int CANCELLED =  1;
    8.  
    9.  //具有这个标识,说明后继节点需要被唤醒
    10.  static final int SIGNAL = -1;
    11.  //Node对象存储标识的地方
    12.  volatile int waitStatus;
    13.  //指向上一个节点
    14.  volatile Node prev;
    15.  //指向下一个节点
    16.  volatile Node next;
    17.  
    18.  //当前Node绑定的线程
    19.  volatile Thread thread;
    20.  
    21.  //返回前驱节点即上一个节点,如果前驱节点为空,抛出异常
    22.  final Node predecessor() throws NullPointerException {
    23.   Node p = prev;
    24.   if (p == null)
    25.    throw new NullPointerException();
    26.   else
    27.    return p;
    28.  }
    29. }

    对于里边的waitStatus属性,我们需要做个解释:(非常重要)

    • CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化;

    • SIGNAL(-1):后面节点等待当前节点唤醒;

    • CONDITION(-2):Condition中使用,当前线程阻塞在Condition,如果其他线程调用了Conditionsignal方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁;

    • PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;

    • 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成;

    AQS 属性

    1. // 头结点
    2. private transient volatile Node head;
    3. // 尾结点
    4. private transient volatile Node tail;
    5. //0->1 拿到锁,大于0 说明当前已经有线程占用了锁资源
    6. private volatile int state;

    今天我们先简单了解下AQS的构造以帮助大家更好的理解ReentrantLock,至于深层次的东西先不做展开!

    加锁

    AQS的结构有了基本了解之后,我们正式进入主题——加锁。从源码中可以看出锁被分为公平锁非公平锁

    1. /**
    2.  * 公平锁代码
    3.  */
    4. final void lock() {
    5.     acquire(1);
    6. }
    7. /**
    8.  * 非公平锁代码
    9.  */
    10. final void lock() {
    11.     if (compareAndSetState(01))
    12.         setExclusiveOwnerThread(Thread.currentThread());
    13.     else
    14.         acquire(1);
    15. }

    初步查看代码发现非公平锁似乎包含公平锁的逻辑,所以我们就从“非公平锁”开始。

    非公平锁

    1. final void lock() {
    2.     //通过 CAS 的方式尝试将 state 从0改为1
    3.     //如果返回 true,代表修改成功,获得锁资源;
    4.     //如果返回false,代表修改失败,未获取锁资源
    5.     if (compareAndSetState(01))
    6.         // 将属性exclusiveOwnerThread设置为当前线程,该属性是AQS的父类提供的
    7.         setExclusiveOwnerThread(Thread.currentThread());
    8.     else
    9.         acquire(1);
    10. }

    compareAndSetState():底层调用的是unsafecompareAndSwapInt,该方法是原子操作;

    假设有两个线程(t1t2)在竞争锁资源,线程1获取锁资源之后,执行setExclusiveOwnerThread操作,设置属性值为当前线程t1

    此时,当t2想要获取锁资源,调用lock()方法之后,执行compareAndSetState(0, 1)返回false,会走else执行acquire()方法。

    方法查看

    1. public final void accquire(int arg) {
    2.     // tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回false
    3.     if (!tryAcquire(arg) &&
    4.         // 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中
    5.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    6.         // 线程中断
    7.         selfInterrupt();
    8. }

    accquire()中涉及的方法比较多,我们将进行拆解,一个一个来分析,顺序:tryAcquire() -> addWaiter() -> acquireQueued()

    查看 tryAcquire() 方法

    1. //AQS中
    2. protected boolean tryAcquire(int arg) {
    3.     //AQS 是基类,具体实现在自己的类中实现,我们去查看“非公平锁”中的实现
    4.     throw new UnsupportedOperationException();
    5. }
    6. //ReentrantLock 中
    7. protected final boolean tryAcquire(int acquires) {
    8.     return nonfairTryAcquire(acquires);
    9. }
    10. final boolean nonfairTryAcquire(int acquires) {
    11.  // 获取当前线程
    12.  final Thread current = Thread.currentThread();
    13.  //获取AQS 的 state 
    14.  int c = getState();
    15.  // 如果 state 为0,代表尝试再次获取锁资源
    16.  if (c == 0) {
    17.   // 步骤同上:通过 CAS 的方式尝试将 state 从0改为1
    18.   //如果返回 true,代表修改成功,获得锁资源;
    19.   //如果返回false,代表修改失败,未获取锁资源
    20.   if (compareAndSetState(0, acquires)) {
    21.    //设置属性为当前线程
    22.    setExclusiveOwnerThread(current);
    23.    return true;
    24.   }
    25.  }
    26.  //当前占有锁资源的线程是否是当前线程,如果是则证明是可重入操作
    27.  else if (current == getExclusiveOwnerThread()) {
    28.   //将 state + 1
    29.   int nextc = c + acquires;
    30.   //为什么会小于 0 呢?因为最大值 + 1 后会将符号位的0改为1 会变成负数(可参考Integer.MAX_VALUE + 1)
    31.   if (nextc < 0// overflow
    32.    //1后小于0,超出锁可重入的最大值,抛异常
    33.    throw new Error("Maximum lock count exceeded");
    34.   //设置 state 状态
    35.   setState(nextc);
    36.   return true;
    37.  }
    38.  return false;
    39. }

    因为线程1已经获取到了锁,此时state为1,所以不走nonfairTryAcquire()if。又因为当前是线程2,不是占有当前锁的线程1,所以也不会走else if,即tryAcquire()方法返回false

    查看 addWaiter() 方法

    走到本方法中,代表获取锁资源失败。addWaiter()将没有获取到锁资源的线程甩到队列的尾部。

    1. private Node addWaiter(Node mode) {
    2.  //创建 Node 类,并且设置 thread 为当前线程,设置为排它锁
    3.  Node node = new Node(Thread.currentThread(), mode);
    4.  // 获取 AQS 中队列的尾部节点
    5.  Node pred = tail;
    6.  // 如果 tail == null,说明是空队列,
    7.  // 不为 null,说明现在队列中有数据,
    8.  if (pred != null) {
    9.   // 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点
    10.   node.prev = pred;
    11.   // CAS 将 tail 节点设置为当前节点
    12.   if (compareAndSetTail(pred, node)) {
    13.    // 将之前尾节点的 next 设置为当前节点
    14.    pred.next = node;
    15.    // 返回当前节点
    16.    return node;
    17.   }
    18.  }
    19.  enq(node);
    20.  return node;
    21. }

    tail不为空,即队列中有数据时,我们来图解一下pred!=null代码块中的代码。初始化状态如下,pred指向尾节点,node指向新的节点。

    node.prev = pred;node的前驱节点设置为pred指向的节点

    compareAndSetTail(pred, node)通过CAS的方式尝试将当前节点node设置为尾结点,此处我们假设设置成功,则FIFO队列的tail指向node节点。

    pred.next = node;pred节点的后继节点设置为node节点,此时node节点成功进入FIFO队列尾部。

    而当pred为空,即队列中没有节点或将node节点设置为尾结点失败时,会走enq()方法。我们列举的例子就符合pred为空的情况,就让我们以例子为基础继续分析吧。

    1. //现在没人排队,我是第一个 || 前边CAS失败也会进入这个位置重新往队列尾巴去塞
    2. private Node enq(final Node node) {
    3.  //死循环
    4.  for (;;) {
    5.   //重新获取tail节点
    6.   Node t = tail;
    7.   // 没人排队,队列为空
    8.   if (t == null) {
    9.    // 初始化一个 Node 为 head,而这个head 没有意义
    10.    if (compareAndSetHead(new Node()))
    11.     // 将头尾都指向了这个初始化的Node,第一次循环结束
    12.     tail = head;
    13.   } else {
    14.    // 有人排队,往队列尾巴塞
    15.    node.prev = t;
    16.    // CAS 将 tail 节点设置为当前节点
    17.    if (compareAndSetTail(t, node)) {
    18.     //将之前尾节点的 next 设置为当前节点
    19.     t.next = node;
    20.     return t;
    21.    }
    22.   }
    23.  }
    24. }

    进入死循环,首先会走if方法的逻辑,通过CAS的方式尝试将一个新节点设置为head节点,然后将tail也指向新节点。可以看出队列中的头节点只是个初始化的节点,没有任何意义。

    继续走死循环中的代码,此时t不为null,所以会走else方法。将node的前驱节点指向t,通过CAS方式将当前节点node设置为尾结点,然后将t的后继节点指向node。此时线程2的节点就被成功塞入FIFO队列尾部。

    查看 acquireQueued()方法

    将已经在队列中的node尝试去获取锁否则挂起。

    1. final boolean acquireQueued(final Node node, int arg) {
    2.  // 获取锁资源的标识,失败为 true,成功为 false
    3.  boolean failed = true;
    4.  try {
    5.   // 线程中断的标识,中断为 true,不中断为 false
    6.   boolean interrupted = false;
    7.   for (;;) {
    8.    // 获取当前节点的上一个节点
    9.    final Node p = node.predecessor();
    10.    //p为头节点,尝试获取锁操作
    11.    if (p == head && tryAcquire(arg)) {
    12.     setHead(node);
    13.     p.next = null;
    14.     // 将获取锁失败标识置为false
    15.     failed = false;
    16.     // 获取到锁资源,不会被中断
    17.     return interrupted;
    18.    }
    19.    // p 不是 head 或者 没拿到锁资源,
    20.    if (shouldParkAfterFailedAcquire(p, node) &&
    21.     // 基于 Unsafe 的 park方法,挂起线程
    22.     parkAndCheckInterrupt())
    23.     interrupted = true;
    24.   }
    25.  } finally {
    26.   if (failed)
    27.    cancelAcquire(node);
    28.  }
    29. }

    这里又出现了一次死循环,首先获取当前节点的前驱节点p,如果p是头节点(头节点没有意义),说明nodehead后的第一个节点,此时当前获取锁资源的线程1可能会释放锁,所以线程2可以再次尝试获取锁。

    假设获取成功,证明拿到锁资源了,将node节点设置为head节点,并将node节点的prethread设置为null。因为拿到锁资源了,node节点就不需要排队了。

    将头节点p的next置为null,此时p节点就不在队列中存在了,可以帮助GC回收(可达性分析)。failed设置为false,表明获取锁成功;interruptedfalse,则线程不会中断。

    如果p不是head节点或者没有拿到锁资源,会执行下边的代码,因为我们的线程1没有释放锁资源,所以线程2获取锁失败,会继续往下执行。

    1. //该方法的作用是保证上一个节点的waitStatus状态为-1(为了唤醒后继节点)
    2. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    3.  //获取上一个节点的状态,该状态为-1,才会唤醒下一个节点。
    4.  int ws = pred.waitStatus;
    5.  // 如果上一个节点的状态是SIGNAL即-1,可以唤醒下一个节点,直接返回true
    6.  if (ws == Node.SIGNAL)
    7.   return true;
    8.  // 如果上一个节点的状态大于0,说明已经失效了
    9.  if (ws > 0) {
    10.   do {
    11.    // 将node 的节点与 pred 的前一个节点相关联,并将前一个节点赋值给 pred
    12.    node.prev = pred = pred.prev;
    13.   } while (pred.waitStatus > 0); // 一直找到小于等于0
    14.   // 将重新标识好的最近的有效节点的 next 指向当前节点
    15.   pred.next = node;
    16.  } else {
    17.   // 小于等于0,但是不等于-1,将上一个有效节点状态修改为-1
    18.   compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    19.  }
    20.  return false;
    21. }

    只有节点的状态为-1,才会唤醒后一个节点,如果节点状态未设置,默认为0。

    图解一下ws>0的过程,因为ws>0的节点为失效节点,所以do...while中会重复向前查找前驱节点,直到找到第一个ws<=0的节点为止,将node节点挂到该节点上。

    我们的pred是头结点且未设置状态,所以状态为0,会走else。通过CAS尝试将pred节点的waitStatus设置为-1,表明node节点需要被pred唤醒。

    shouldParkAfterFailedAcquire()返回false,继续执行acquireQueued()中的死循环。

    步骤和上边一样,node的前驱节点还是head,继续尝试获取锁。如果线程1释放了锁,线程2就可以拿到,返回true;否则继续调用shouldParkAfterFailedAcquire(),因为上一步已经将前驱结点的ws设置为-1了,所以直接返回true

    执行parkAndCheckInterrupt()方法,通过UNSAFE.park();方法阻塞当前线程2。等以后执行unpark方法的时候,如果node是头节点后的第一个节点,会进入acquireQueued()方法中走if (p == head && tryAcquire(arg))的逻辑获取锁资源并结束死循环。

    查看cancelAcquire()方法

    该方法执行的机率约等于0,为什么这么说呢?因为针对failed属性,只有JVM内部出现问题时,才可能出现异常,执行该方法。

    1. // node 为当前节点
    2. private void cancelAcquire(Node node) {
    3.  if (node == null)
    4.   return;
    5.  node.thread = null;
    6.  // 上一个节点
    7.  Node pred = node.prev;
    8.  // 节点状态大于0,说明节点失效
    9.  while (pred.waitStatus > 0)
    10.   node.prev = pred = pred.prev;
    11.  // 将第一个不是失效节点的后继节点声明出来
    12.  Node predNext = pred.next;
    13.  // 节点状态变为失效
    14.  node.waitStatus = Node.CANCELLED;
    15.  // node为尾节点,cas设置pred为尾节点
    16.  if (node == tail && compareAndSetTail(node, pred)) {
    17.   //cas将pred的next设置为null
    18.   compareAndSetNext(pred, predNext, null);
    19.  } else {
    20.   int ws;
    21.   // 中间节点
    22.   // 如果上一个节点不是head 节点
    23.   if (pred != head &&
    24.    ((ws = pred.waitStatus) == Node.SIGNAL ||
    25.     // 前边已经判断了大于0的操作,
    26.     // pred 是需要唤醒后继节点的,所以当 waitStatus 不为 -1 时,需要将 pred 节点的 waitStatus 设置为 -1 
    27.     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
    28.    pred.thread != null) {
    29.    Node next = node.next;
    30.    if (next != null && next.waitStatus <= 0)
    31.     // CAS 尝试将 pred 的 next 指向当前节点的 next
    32.     compareAndSetNext(pred, predNext, next);
    33.   } else {
    34.    // head 节点,唤醒后继节点
    35.    unparkSuccessor(node);
    36.   }
    37.   node.next = node; // help GC
    38.  }
    39. }

    执行到while时找到前驱节点中最近的有效节点,把当前节点node挂到有效节点后边,可以过滤掉当前节点前的失效节点。声明出有效节点的第一个后继无效节点predNext,并把当前的node节点状态设置为失效状态。

    if中的操作:如果当前节点是尾节点,CAS尝试将最近的有效节点设置为尾节点,并将尾节点的next设置为null

    else中的操作:

    如果pred节点不是头结点即中间节点,并且predwaitStatus为-1或者waitStatus<=0,为了让pred节点能唤醒后继节点,需要设置为-1,并且pred节点的线程不为空。获取node节点的后继节点,如果后继节点有效,CAS尝试将prednext指向node节点的next

    当其他节点来找有效节点的时候走当前nodeprev这条线,而不是再一个一个往前找,可以提高效率。

    如果是头结点则唤醒后继节点。

    最后将node节点的next指向自己。

    解锁

    释放锁是不区分公平锁和非公平锁的,释放锁的核心是将state由大于 0 的数置为 0。废话不多说,直接上代码

    1. //释放锁方法
    2. public void unlock() {
    3.  sync.release(1);
    4. }
    5. public final boolean release(int arg) {
    6.   //尝试释放锁资源,如果释放成功,返回true
    7.  if (tryRelease(arg)) {
    8.   Node h = head;
    9.   // head 不为空且 head 的 ws 不为0(如果为0,代表后边没有其他线程挂起)
    10.   if (h != null && h.waitStatus != 0)
    11.    // AQS的队列中有 node 在排队,并且线程已经挂起
    12.    // 需要唤醒被挂起的 Node
    13.    unparkSuccessor(h);
    14.   return true;
    15.  }
    16.  // 代表释放一次没有完全释放
    17.  return false;
    18. }

    如果释放锁成功,需要获取head节点。如果头结点不为空且waitStatus不为0,则证明有node在排队,执行唤醒挂起其他node的操作。

    查看tryRelease()方法

    1. protected final boolean tryRelease(int releases) {
    2.  //获取当前锁的状态,先进行减1操作,代表释放一次锁资源
    3.  int c = getState() - releases;
    4.  //如果释放锁的线程不是占用锁的线程,直接抛出异常
    5.  if (Thread.currentThread() != getExclusiveOwnerThread())
    6.   throw new IllegalMonitorStateException();
    7.  boolean free = false;
    8.  // 如果 c 为0 ,代表锁完全释放了,如果不为0,代表锁之前重入了,一次没释放掉,等待下次再次执行时,再次判断
    9.  if (c == 0) {
    10.   // 释放锁标志为 true,代表完全释放了
    11.   free = true;
    12.   // 将占用互斥锁的标识置为 null
    13.   setExclusiveOwnerThread(null);
    14.  }
    15. // 设置 state 状态
    16.  setState(c);
    17.  return free;
    18. }

    我们的例子中线程1占用锁资源,线程1释放锁之后,state为0。进入if操作,将释放标志更新为true,将FIFO队列的exclusiveOwnerThread标志置为null

    查看unparkSuccessor()方法

    用于唤醒AQS中被挂起的线程。

    1. // 注意当前的 node 节点是 head 节点
    2. private void unparkSuccessor(Node node) {
    3.  //获取 head 的状态
    4.  int ws = node.waitStatus;
    5.  if (ws < 0)
    6.   // CAS 将 node 的 ws 设置为0,代表当前 node 接下来会舍弃
    7.   compareAndSetWaitStatus(node, ws, 0);
    8.  // 获取头节点的下一个节点
    9.  Node s = node.next;
    10.  // 如果下一个节点为null 或者 下一个节点为失效节点,需要找到离 head 最近的有效node
    11.  if (s == null || s.waitStatus > 0) {
    12.   s = null;
    13.   // 从尾节点开始往前找不等于null且不是node的节点
    14.   for (Node t = tail; t != null && t != node; t = t.prev)
    15.    // 如果该节点有效,则将s节点指向t节点
    16.    if (t.waitStatus <= 0)
    17.     s = t;
    18.  }
    19.   // 找到最近的node后,直接唤醒
    20.  if (s != null)
    21.   LockSupport.unpark(s.thread);
    22. }

    问题解析:为什么要从尾结点往前查找呢?

    因为在addWaiter方法中是先给prev指针赋值,最后才将上一个节点的next指针赋值,为了避免防止丢失节点或者跳过节点,必须从后往前找。

    我们举例中head节点的状态为-1,通过CAS的方式将head节点的waitStatus设置为0。

    我们的头结点的后继节点是线程2所在的节点,不为null,所以这边会执行unpark操作,从上边的acquireQueued()内的parkAndCheckInterrupt()方法继续执行。

    1. private final boolean parkAndCheckInterrupt() {
    2.     LockSupport.park(this);
    3.     //返回目标线程是否中断的布尔值:中断返回true,不中断返回false,且返回后会重置中断状态为未中断
    4.     return Thread.interrupted();
    5. }

    因为线程2未中断,所以返回false。继续执行acquireQueued()中的死循环

    1. for (;;) {
    2.     // 获取当前节点的上一个节点
    3.     final Node p = node.predecessor();
    4.     //p为头节点,尝试获取锁操作
    5.     if (p == head && tryAcquire(arg)) {
    6.         setHead(node);
    7.         p.next = null;
    8.         // 将获取锁失败标识置为false
    9.         failed = false;
    10.         // 获取到锁资源,不会被中断
    11.         return interrupted;
    12.     }
    13.     // p 不是 head 或者 没拿到锁资源,
    14.     if (shouldParkAfterFailedAcquire(p, node) &&
    15.         // 基于 Unsafe 的 park方法,挂起线程
    16.         parkAndCheckInterrupt())
    17.         interrupted = true;
    18. }

    此时p是头节点,且能获取锁成功,将exclusiveOwnerThread设置为线程2,即线程2 获取锁资源。

    node节点设置为head节点,并将node节点的prethread设置为null。因为拿到锁资源了,node节点就不需要排队了。

    将头节点p的next置为null,此时p节点就不在队列中存在了,可以帮助GC回收(可达性分析)。failed设置为false,表明获取锁成功;interruptedfalse,则线程不会中断。

    为什么被唤醒的线程要调用Thread.interrupted()清除中断标记

    从上边的方法可以看出,当parkAndCheckInterrupt()方法返回true时,即Thread.interrupted()方法返回了true,也就是该线程被中断了。为了让被唤醒的线程继续执行后续获取锁的操作,就需要让中断的线程像没有被中断过一样继续往下执行,所以在返回中断标记的同时要清除中断标记,将其设置为false

    清除中断标记之后不代表该线程不需要中断了,所以在parkAndCheckInterrupt()方法返回true时,要自己设置一个中断标志interrupted = true,为的就是当获取到锁资源执行完相关的操作之后进行中断补偿,故而需要执行selfInterrupt()方法中断线程。

    以上就是我们加锁解锁的图解过程了。最后我们再来说一下公平锁和非公平锁的区别。

    区别

    前边已经说过了,似乎非公平锁包含了公平锁的全部操作。打开公平锁的代码,我们发现accquire()方法中只有该方法的实现有点区别。

    hasQueuedPredecessors()返回false时才会尝试获取锁资源。该方法代码实现如下

    1. public final boolean hasQueuedPredecessors() {
    2.     Node t = tail; 
    3.     Node h = head;
    4.     Node s;
    5.     return h != t &&
    6.         ((s = h.next== null || s.thread != Thread.currentThread());
    7. }
    • h==t时,队列为空,表示没人排队,可以获取锁资源;

    • 队列不为空,头结点有后继节点不为空且s节点获取锁的线程是当前线程也可以获取锁资源,代表锁重入操作;

    总结

    以上就是我们的全部内容了,我们在最后再做个总结:

    • 代码使用要合乎规范,避免加锁成功后,在finally中无法解锁;

    • 理解AQSFIFO队列以及Node的相关属性,尤其注意waitStatus的状态;

    • 利用图加深对非公平锁源码的理解;

  • 相关阅读:
    初级前端开发岗
    Spring的Bean加载流程
    [网络] 前端大文件上传
    10个好用的Mac数据恢复软件推荐—恢复率高达99%
    qsort 函数的使用
    计算机网络-----传输层的概述
    HTML5期末大作业:游戏网站设计与实现——基于bootstrap响应式游戏资讯网站制作HTML+CSS+JavaScript...
    【奇思妙想】【节省磁盘空间】我有一些文件,我不想移动它们,但又想节省磁盘空间,该怎么做呢?
    “蔚来杯“2022牛客暑期多校训练营2,签到题GJK
    linux网络编程中的errno处理
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/127424077