• 重点,一文掌握ReentrantLock加解锁原理!|原创


    本文详细讲解了 ReentrantLock 加锁和释放锁的原理,以及和 Synchronized 的对比。本文较长,建议收藏!

    点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

    简要总结 ReentrantLock

    实现原理:volatile 变量 + CAS设置值 + AQS + 两个队列

    实现阻塞:同步队列 + CAS抢占标记为 valatile 的 state

    实现等待唤醒:await :持有锁,park ->加入等待队列 ;signal:唤醒下一个等待队列节点,转移进入同步队列,然后CAS抢占或者按照阻塞队列等待抢占。接着 await 后续内容程序得以继续执行。

    ReentrantLock 结构分析

    ReentrantLock 继承了Lock接口, lock方法实际上是调用了Sync的子类NonfairSync(非公平锁)的lock方法。ReentrantLock的真正实现在他的两个内部类NonfairSync 和 FairSync中,默认实现是非公平锁。并且内部类都继承于内部类Sync,而Sync根本的实现则是大名鼎鼎的 AbstractQueuedSynchronizer 同步器(AQS)。

    ace281765102e06b86ef3b69d4834175.png

    具体详见如下代码:

    1. public class ReentrantLock implements Lock, java.io.Serializable {
    2.     private static final long serialVersionUID = 7373984872572414699L;
    3.     /** Synchronizer providing all implementation mechanics */
    4.     private final Sync sync;
    5.   
    6.   public ReentrantLock() {
    7.         sync = new NonfairSync();
    8.     }
    9.      abstract static class Sync extends AbstractQueuedSynchronizer {
    10.        ……省略代码
    11.      }
    12.   
    13.   //非公平锁
    14.   static final class NonfairSync extends Sync {
    15.         private static final long serialVersionUID = 7316153563782823691L;
    16.         /**
    17.          * Performs lock.  Try immediate barge, backing up to normal
    18.          * acquire on failure.
    19.          */
    20.         final void lock() {
    21.             if (compareAndSetState(01))
    22.                 setExclusiveOwnerThread(Thread.currentThread());
    23.             else
    24.                 acquire(1);
    25.         }
    26.         protected final boolean tryAcquire(int acquires) {
    27.             return nonfairTryAcquire(acquires);
    28.         }
    29.     }
    30.   // 公平锁
    31.   static final class FairSync extends Sync {
    32.         private static final long serialVersionUID = -3000897897090466540L;
    33.         final void lock() {
    34.             acquire(1);
    35.         }
    36.         /**
    37.          * Fair version of tryAcquire.  Don't grant access unless
    38.          * recursive call or no waiters or is first.
    39.          */
    40.         protected final boolean tryAcquire(int acquires) {
    41. ……省略
    42.         }
    43.     }
    44.   // lock 方法本质就是调用sync类
    45.   public void lock() {
    46.         sync.lock();
    47.     }
    48. }

    lock 加锁过程

    按照调用 lock 方法是否抢占锁成功,可以以调用 park 方法为界限,将加锁的过程分为两部分:一部分是当前线程被阻塞前,另一部分是线程被唤醒继续执行后。(这里以非公平锁为例)

    阻塞前

    1.直接通过CAS尝试获取锁,设置state为1。如果获取成功则将锁标识设为独占,就是是将当前线程设置给 exclusiveOwnerThread。

    1. final void lock() {
    2.     if (compareAndSetState(01))
    3.         setExclusiveOwnerThread(Thread.currentThread());
    4.     else
    5.         acquire(1);
    6. }

    2.如果获取失败,再次尝试获取,调用acquire。

    3.tryAcquire ->:判断锁是否被占有,如果空闲则再次尝试CAS获取锁;如果已被占有则对比占有锁的线程是否为本线程,是的话将state+1,这就是可重入锁的关键逻辑。

    1. //AbstractQueuedSynchronizer
    2. public final void acquire(int arg) {
    3.     if (!tryAcquire(arg) &&
    4.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    5.         selfInterrupt();
    6. }
    7. //ReentrantLock.NonfairSync
    8. protected final boolean tryAcquire(int acquires) {
    9.     return nonfairTryAcquire(acquires);
    10. }
    11. //ReentrantLock.Sync
    12. final boolean nonfairTryAcquire(int acquires) {
    13.     final Thread current = Thread.currentThread();
    14.     int c = getState();
    15.     if (c == 0) {
    16.       // cas再次尝试获取
    17.         if (compareAndSetState(0, acquires)) {
    18.             setExclusiveOwnerThread(current);
    19.             return true;
    20.         }
    21.     }
    22.     else if (current == getExclusiveOwnerThread()) {
    23.       // 可重入逻辑
    24.         int nextc = c + acquires;
    25.         if (nextc < 0// overflow
    26.             throw new Error("Maximum lock count exceeded");
    27.         setState(nextc);
    28.         return true;
    29.     }
    30.     return false;
    31. }

    4.如果获取失败则将节点插入队列尾部,如果队列为空,则会初始化队列,并且设置头尾节点为空节点,再将Node设为尾节点。

    1. // 获取锁失败
    2. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    3. // 加入同步队列
    4. private Node addWaiter(Node mode) {
    5.     Node node = new Node(Thread.currentThread(), mode);
    6.     // Try the fast path of enq; backup to full enq on failure
    7.     Node pred = tail;
    8.     if (pred != null) {
    9.         node.prev = pred;
    10.       // 通过CAS设置尾节点为当前节点,前驱节点为之前的尾节点。
    11.         if (compareAndSetTail(pred, node)) {
    12.             pred.next = node;
    13.             return node;
    14.         }
    15.     }
    16.   // 如果当前链表为空,则在此处进行初始化
    17.     enq(node);
    18.     return node;
    19. }
    20. private Node enq(final Node node) {
    21.     for (;;) {
    22.         Node t = tail;
    23.         if (t == null) { // Must initialize
    24.             if (compareAndSetHead(new Node()))
    25.                 tail = head;
    26.         } else {
    27.           // 追加到队列尾
    28.             node.prev = t;
    29.             if (compareAndSetTail(t, node)) {
    30.                 t.next = node;
    31.                 return t;
    32.             }
    33.         }
    34.     }
    35. }

    5.将新建的Node传入acquireQueued,获取前驱节点,如果节点就是head 头节点,那么尝试CAS竞争锁(head随时释放)。如果抢占成功将头节点设为自己。

    1. final boolean acquireQueued(final Node node, int arg) {
    2.     boolean failed = true;
    3.     try {
    4.         boolean interrupted = false;
    5.         for (;;) {
    6.             final Node p = node.predecessor();
    7.           // 如果是头节点,再次尝试
    8.             if (p == head && tryAcquire(arg)) {
    9.                 setHead(node);
    10.                 p.next = null; // help GC
    11.                 failed = false;
    12.                 return interrupted;
    13.             }
    14.             if (shouldParkAfterFailedAcquire(p, node) &&
    15.                 parkAndCheckInterrupt())
    16.                 interrupted = true;
    17.         }
    18.     } finally {
    19.         if (failed)
    20.             cancelAcquire(node);
    21.     }
    22. }

    6.如果没有抢占成功,则进入shouldParkAfterFailedAcquire逻辑,将前驱节点设置为Signal,表示后继节点(也就是当前节点)需要前驱节点去唤醒。设置完之后再次进入自旋锁,尝试获得锁。

    关于Node的状态这里说明一下:

    节点刚创建的时候,status=0,假设这时候本节点就是head节点,那么他会进入else逻辑,将自身状态设置为Signal,然后再次进入自旋,尝试获取锁。如果还是没有获取到锁,那么再次进入shouldParkAfterFailedAcquire方法后会进入第一个if逻辑,方法返回True。

    1. /**
    2. * Checks and updates status for a node that failed to acquire.
    3. * Returns true if thread should block. This is the main signal
    4. * control in all acquire loops.  Requires that pred == node.prev.
    5. * 如果获取锁失败,检查并且更新节点。如果需要被park阻塞,返回true。
    6. * 在所有的循环逻辑中,这是主要的信号控制逻辑。
    7. *
    8. * pred:表示前驱节点
    9. * node:表示当前线程节点
    10. */
    11. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    12.     int ws = pred.waitStatus;
    13.     if (ws == Node.SIGNAL)
    14.        // 第二次尝试获取锁会进入这段逻辑
    15.         /*
    16.          * This node has already set status asking a release
    17.          * to signal it, so it can safely park.
    18.          */
    19.          // 表明线程已经准备好被阻塞并等待之后被唤醒
    20.         return true;
    21.     if (ws > 0) {
    22.         /*
    23.          * Predecessor was cancelled. Skip over predecessors and
    24.          * indicate retry.
    25.          */
    26.          // 若pred.waitStatus状态位大于0,说明这个前驱点已经取消了获取锁的操作,
    27.          // doWhile循环会递归删除掉这些放弃获取锁的节点
    28.         do {
    29.             node.prev = pred = pred.prev;
    30.         } while (pred.waitStatus > 0);
    31.         pred.next = node;
    32.     } else {
    33.         /*
    34.          * 节点刚创建的时候,status=0,逻辑会走到这里将自身状态设置为signal
    35.          * waitStatus must be 0 or PROPAGATE.  Indicate that we
    36.          * need a signal, but don't park yet.  Caller will need to
    37.          * retry to make sure it cannot acquire before parking.
    38.          */
    39.          //若状态位不为Node.SIGNAL,且没有取消操作,则会尝试将前驱节点状态位修改为Node.SIGNAL
    40.         // 表示将会唤醒后继节点
    41.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    42.     }
    43.     return false;
    44. }

    7.第二次自旋获取失败后,由于前驱节点已经是Signal,这时进入parkAndCheckInterrupt,将当前线程阻塞,等待被唤醒。后续其他线程如果也尝试抢占锁,会同样被阻塞。

    1. private final boolean parkAndCheckInterrupt() {
    2.   // 阻塞线程
    3.     LockSupport.park(this);
    4.   // 线程继续执行
    5.     return Thread.interrupted();
    6. }

    park方法被唤醒后

    在其他线程释放锁资源后,唤醒下一个节点,park的后半部分逻辑继续执行。

    1. 继续执行之前Park之后的逻辑,在此处线程被唤醒。这里会返回中断标记,这也是为什么ReentrantLock可以相应中断的原因。

    947bba3358a922f1bc9d0b26dce803e8.png
    1. 然后再次进入自旋锁,使用CAS获取到锁标记,将头节点设为当前节点,然后返回中断标记跳出循环。

    2. 至此,获取锁流程结束。

    unlock 释放锁过程

    1.尝试释放锁,用state减去1,判断是否等于0。如果等于0表示已经完全释放锁,将线程标记设为null。否则释放失败,表示当前线程仍在继续持有,继续持有说明有重入情况。

    1. // ReentrantLock
    2. public void unlock() {
    3.     sync.release(1);
    4. }
    5. // AQS
    6. public final boolean release(int arg) {
    7.   // 释放锁
    8.     if (tryRelease(arg)) {
    9.         Node h = head;
    10.         if (h != null && h.waitStatus != 0)
    11.           // 唤醒后继节点
    12.             unparkSuccessor(h);
    13.         return true;
    14.     }
    15.     return false;
    16. }
    17. // 释放锁
    18. protected final boolean tryRelease(int releases) {
    19.     int c = getState() - releases;
    20.     if (Thread.currentThread() != getExclusiveOwnerThread())
    21.         throw new IllegalMonitorStateException();
    22.     boolean free = false;
    23.     if (c == 0) {
    24.         free = true;
    25.       // 释放锁
    26.         setExclusiveOwnerThread(null);
    27.     }
    28.     setState(c);
    29.     return free;
    30. }

    2.拿到头节点,然后解锁后继节点。如果当前节点状态小于0(signal=-1),则修改节点status为0。然后向后递归找到status小于等于0的节点(正常为0),调用unpark解除阻塞。返回解锁成功。

    1. // 唤醒后继节点
    2. private void unparkSuccessor(Node node) {
    3.     /*
    4.      * If status is negative (i.e., possibly needing signal) try
    5.      * to clear in anticipation of signalling.  It is OK if this
    6.      * fails or if status is changed by waiting thread.
    7.      */
    8.     int ws = node.waitStatus;
    9.     if (ws < 0)
    10.         compareAndSetWaitStatus(node, ws, 0);
    11.     /*
    12.      * Thread to unpark is held in successor, which is normally
    13.      * just the next node.  But if cancelled or apparently null,
    14.      * traverse backwards from tail to find the actual
    15.      * non-cancelled successor.
    16.      */
    17.   // 拿到下一个节点
    18.     Node s = node.next;
    19.   //要解除阻塞的线程在后继节点中,通常只是下一个节点。但如果取消或明显为空,则从尾部向前遍历以找到实际未取消的继任者。
    20.     if (s == null || s.waitStatus > 0) {
    21.         s = null;
    22.         for (Node t = tail; t != null && t != node; t = t.prev)
    23.             if (t.waitStatus <= 0)
    24.                 s = t;
    25.     }
    26.     if (s != null)
    27.       //解锁
    28.         LockSupport.unpark(s.thread);
    29. }
    cbb020a87ff4b9897e58151568550573.png

    3.在这之后便继续开始执行之前被阻塞的线程中的逻辑。

    到这里 ReentrantLock 的加解锁过程原理便讲解结束,关于条件队列的内容,有兴趣后续文章会做讲解。

    对比 Synchronized

    既然已经了解了 ReentrantLock ,那么在此对大家所熟知的 Synchronized 进行一个对比。

    与Synchronized相同点:

    1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

    但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

    2.ReentrantLock和synchronized都是可重入锁。

    synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

    3.都可以实现线程之间的等待通知机制。使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

    与Synchronized 不同点:

    1. ReentrantLock是Java层面的实现,synchronized是JVM层面的实现。

    2. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),而ReentrantLock需要手动释放锁需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。

    3. synchronized是非公平锁,ReentrantLock可以实现公平和非公平锁。

    4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。配合重试机制更好的解决死锁。

    5. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。

    6. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。

    7. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。

    最后,欢迎大家提问和交流。

    如果对你有帮助,欢迎点赞、评论或分享,感谢阅读!

    update在MySQL中是怎样执行的,一张图牢记|原创

    2022-11-19

    8347b3ec709c87f58c96f4e5d2f65d66.jpeg

    从二叉查找树到B*树,一文搞懂搜索树的演进!|原创

    2022-11-14

    410ed804bea17b122f9712a5ab84b1e0.jpeg

    问到ThreadLocal,看这一篇就够了|原创

    2022-10-13

    6da4801fb6b9d1b47120e564dc9536f6.jpeg
  • 相关阅读:
    PHP检测一个字符串中是否包含另外一个字符或字符串
    2. Layui栅格系统和布局
    CentOS 升级内核至5.15.52
    真空腔体的设计要点
    【经验分享】嵌入式C语言开发如何有效地排查内存泄露的疑难问题?
    什么是Monkey,以及Monkey异常
    杭电多校3 1012. Two Permutations dp*
    【牛客刷题--SQL篇】多表查询链接查询 SQL22统计每个学校的答过题的用户的平均答题数
    循环神经网络RNN+长短期记忆网络LSTM 学习记录
    Mac M芯片上安装统信UOS 1070arm64虚拟机
  • 原文地址:https://blog.csdn.net/sinat_32873711/article/details/128030617