• AQS之ReentrantLock源码解析


    • 对于ReentrantLock源码关注点在哪里
      1. ReentrantLock加锁解锁的逻辑。

      2. 公平和非公平,可重入锁的实现。

      3. 并发场景下入队和出队的操作。

    • 代码准备
    1. public class ReentrantLockDemo1 {
    2. private static int sum = 0;
    3. private static Lock lock = new ReentrantLock();
    4. public static void main(String[] args) throws InterruptedException {
    5. for(int i=0;i<3;i++){
    6. Thread t = new Thread(()->{
    7. lock.lock();
    8. try {
    9. for(int j=0;j<10000;j++){
    10. sum++;
    11. }
    12. }finally {
    13. lock.unlock();
    14. }
    15. });
    16. t.start();
    17. }
    18. Thread.sleep(2000);
    19. System.out.println(sum);
    20. }
    21. }

    一个简单的代码,我们打上断点,选择线程的模式,来看看线程内部是如何执行的。

    • debug执行过程
      • 首先选择thread0来执行,看看上锁过程

     两行代码。

    compareAndSetState(0, 1)通过cas运算将锁状态由0改为1

    setExclusiveOwnerThread(Thread.currentThread()) 将锁绑定到该线程,上锁完成。

             通过上面两行逻辑完成上锁。此时thread0以执行完成,sum值为10000,但是暂未释放锁。我们选定thread1进入上锁逻辑,来看看thread1是如何执行的。

            此时锁状态为1,cas失败,执行acquire逻辑。

     我们跟进acquire方法,首先执行tryAcquire,我们跟进去看看

            可见c是锁的状态,在thread0上锁时,已通过cas将其改为1,所以第一个if判断是不进入的。锁以绑定thread0,所以第二个if也是不进入的,直接返回false。第二个if判断就是可重入部分的逻辑。

    判断条件为&&关联,!tryAcquire则为true,继续后一半判断逻辑,我们进入addWaiter方法

            创建绑定当前线程的node节点。将pred节点设置为tail,此时tail为null,链表还未组成,进入enq入队方法。

             发现进入一个自旋。首先将tail赋值给一个空节点t。如果t为空,则compareAndSetHead(new Node())设置头节点为空白节点。最后头,尾节点均未同一个空白节点构成链表。

            

            然后进入下一循环, 此时t节点赋值tail则不为空(虽然这个节点的属性为空白),进入else结构。我们传入的需要入队的节点node的prev赋值t,通过cas将node节点设置为tail。最后将node节点赋值给t.next。此时完成了我们node节点入队的逻辑。我们得到了头结点为t,尾节点为我们入队的node节点的双向链表。

            返回绑定thread1的node节点,我们进入acquireQueued的方法。首先获得条件等待队列的头结点p。在第一个if中会尝试再次加锁,失败返回false,跳入下个if判断中。

             我们进入shouldParkAfterFailedAcquire方法。头结点是一个未传入任何属性的空白节点,该节点的waitStatus为0,通过cas我们将头节点的waitStatus改为可唤醒后面节点的状态-1并返回false。

             因为返回false,则不执行后半部分判断,通过自旋开始下一次循环。此时的头节点p的waitStatus为-1,在次执行shouldParkAfterFailedAcquire()方法后返回true,我们进入parkAndCheckInterrupt()方法。

     最后阻塞线程。

    我们在来走thread2, 同样的进入acquire方法。

     tryAcquire方法不成功返回false,进入addWaiter()的入队方法。此时pred节点等于尾节点就是绑定thread1的节点,pred不为空进入if逻辑。将当前节点的prev指向thread1的节点,通过cas将当前节点设置为尾节点,最后将thread1节点的next指向当前节点,完成当前节点的入队,返回当前节点。

            最后就是讲thread1节点的waitStatus改为-1,并在第二轮循环中阻塞thread2线程。 

            我们在返回thread0来看解锁逻辑。可以看到就是讲锁状态置为0,同时将绑定的线程置为null,返回true。

             进入if里面的逻辑,头结点不为空且状态不为0,执行unparkSuccessor(h)

            

            进入第一个if判断,将头结点的waitStatus置为0。然后将thread1的node节点赋值给s节点,s节点不为空,唤醒s节点

     

     

            此时thread1从park中唤醒

     

             然后重新进行入队操作方法的循环中,获取当前节点的前一个节点p,p为头节点,然后尝试加锁。加锁成功后进入第一个if逻辑中。

     

            setHead就是将当前节点(thread1)设置为头节点,并将当前节点所有属性置为null,包括绑定的thread。

            然后将链表中第一个节点的next属性置为空,返回0。此时头结点出队,链表缩减为两个节点。

     

    总结 :

    1. thread0上锁,上锁逻辑,将锁状态由0变为1同时绑定线程thread0;
    2. thread1上锁,不成功;进入acquire()方法,再次尝试上锁失败;
    3. 进入addWaiter()方法,进入enq()方法;
    4. 在enq()方法中自旋,第一次会构建一个空节点的链表;第二次循环入队成功构成一个两个节点组成的链表。
    5. 返回当前节点(此时链表中第二个节点,即绑定thread1的节点),进入acquireQueued()方法;
    6. 进入自旋,再次尝试获取锁失败,进入shouldParkAfterFailedAcquire()方法;
    7. shouldParkAfterFailedAcquire()方法会将两节点链表中的头结点的waitStatus属性由0变为-1可唤醒状态,并返回false,直接进入acquireQueued()方法中的第二次循环。
    8. 再次尝试加锁失败,shouldParkAfterFailedAcquire()方法返回true,执行parkAndCheckInterrupt()方法,通过LockSupport.park()方法阻塞住线程。
    9. thread2上锁,在acquire()方法中再次加锁失败,进入addWaiter()方法。
    10. 因为链表已经存在,直接将node入队构成三节点链表,返回当前node节点(thread2)。
    11. 进入acquireQueued()方法的自旋中,第一次将第二个节点的waitStatus属性改为-1,第二次循环直接阻塞。
    12. 此时三节点链表完全构成,第一个和第二个节点的waitStatus为-1,第二个节点绑定thread1,第二个节点绑定thread2.
    13. thread0解锁,tryRelease()方法中将锁状态设为0,接触绑定的thread0线程,返回true。
    14. 进入unparkSuccessor()方法,将头结点的waitStatus设为0,通过LockSupport.unpark()方法将第二个节点即绑定thread1的节点唤醒。
    15. 此时thread1在acquireQueued()方法中的自旋中再次尝试加锁成功,进入第一个if逻辑中。
    16. 在这个if中会将三节点链表中的第二个节点设置为头结点,然后将第二个节点绑定的thread1线程置空,在剔除头结点。
    17. 最后三节点链表变为两节点链表。

     

  • 相关阅读:
    微服务从代码到k8s部署应有尽有系列(七、支付服务)
    redis中使用pipeline批量执行命令,提升性能
    深入浅出MySQL Server System Variables
    3Dslicer医学图像三维坐标系(xyz,RAS,IJK)差异及处理
    【备忘】websocket学习之挖坑埋自己
    发现一不错的编程助手 Amazon CodeWhisperer
    二叉树—相关计算题
    巧用@Conditional注解根据配置文件注入不同的bean对象
    声音克隆,定制自己的声音,使用最新版Bert-VITS2的云端训练+推理记录
    如何在Windows 10上打开和关闭平板模式?这里提供详细步骤
  • 原文地址:https://blog.csdn.net/yfyh2021/article/details/125389854