• 释放锁流程源码剖析


    1 释放锁流程概述

    ReentrantLock的unlock()方法不区分公平锁还是非公平锁。

    • 首先调用unlock()方法。

    • unlock()底层使用的是Sync.release(1)方法

    •  public void unlock() {
           sync.release(1);
       }

     release(1)方法会调用tryRelease(1)去尝试解锁。

    1. public final boolean release(int arg) {<!-- -->
    2. //尝试释放锁
    3. if (tryRelease(arg)) {<!-- -->
    4. Node h = head;
    5. if (h != null && h.waitStatus != 0)
    6. //如果释放锁成功,而且等待队列不为空,且有一个以上的等待线程
    7. //因为只有下一个线程才能将前一个线程的waitStatus的状态改为-1,head表示当前执行的线程
    8. //当head不为空,且waitStatus !=0说明有等待线程初始化了等待队列,且将持有锁线程的
    9. //等待状态改为了-1,必然存在等待线程,将队头的第一个唤醒
    10. unparkSuccessor(h);
    11. return true;
    12. }
    13. return false;
    14. }

     tryRelease(arg)尝试释放锁

    1. @ReservedStackAccess
    2. protected final boolean tryRelease(int releases) {<!-- -->
    3. //释放一次锁,就将重入的次数减掉1
    4. int c = getState() - releases;
    5. if (Thread.currentThread() != getExclusiveOwnerThread())
    6. throw new IllegalMonitorStateException();
    7. boolean free = false;
    8. //如果锁得状态为1,则表示锁真正被释放了,将持有锁的线程置为null
    9. if (c == 0) {<!-- -->
    10. free = true;
    11. setExclusiveOwnerThread(null);
    12. }
    13. //否则,锁依然被持有,因为该锁被持锁线程重入了多次
    14. setState(c);
    15. return free;
    16. }

     如果tryRelease()释放锁成功,且判断等待队列确实有阻塞线程,则尝试唤醒

    1. private void unparkSuccessor(Node node) {<!-- -->
    2. //如果等待的线程状态<0,SIGNAL,将其设为0
    3. int ws = node.waitStatus;
    4. if (ws < 0)
    5. node.compareAndSetWaitStatus(ws, 0);
    6. Node s = node.next;
    7. //找一个符合条件,即真正在阻塞睡眠的线程
    8. if (s == null || s.waitStatus > 0) {<!-- -->
    9. s = null;
    10. for (Node p = tail; p != node && p != null; p = p.prev)
    11. if (p.waitStatus <= 0)
    12. s = p;
    13. }
    14. //找到后,将其唤醒。有个疑问,头节点不变化嘛???
    15. if (s != null)
    16. LockSupport.unpark(s.thread);
    17. }

     回答自己的疑问,为啥没有操作头节点呢?这是因为唤醒阻塞的第一个线程后,它会重新去获取锁,而不是直接将锁分配给它。

    1. final boolean acquireQueued(final Node node, int arg) {
    2. boolean interrupted = false;
    3. try {
    4. for (;;) {
    5. final Node p = node.predecessor();
    6. if (p == head && tryAcquire(arg)) {
    7. setHead(node);
    8. p.next = null; // help GC
    9. return interrupted;
    10. }
    11. if (shouldParkAfterFailedAcquire(p, node))
    12. //从此处被唤醒后,重新进行循环,尝试去争抢锁,如果没抢到,则继续阻塞(非公平的时候)
    13. //当刚被唤醒,循环一次,此时p==head,同时如果tryAcquire(1)去获得锁,
    14. //如果获得成功将自己设置为head,
    15. //如果获得锁失败,则自己再自旋一次(因为在释放锁的时候,head的ws又重置为0了).
    16. //如果还是失败,则自己再次park()睡眠
    17. interrupted |= parkAndCheckInterrupt();
    18. }
    19. } catch (Throwable t) {
    20. cancelAcquire(node);
    21. if (interrupted)
    22. selfInterrupt();
    23. throw t;
    24. }
    25. }

    2 释放锁源码分析

    1. public void unlock() {
    2. // 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
    3. sync.release(1);
    4. }
    5. // 释放锁的核心流程
    6. public final boolean release(int arg) {
    7. // 核心释放锁资源的操作之一
    8. if (tryRelease(arg)) {
    9. // 如果锁已经释放掉了,走这个逻辑
    10. Node h = head;
    11. // h不为null,说明有排队的(录课时估计脑袋蒙圈圈。)
    12. // 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
    13. if (h != null && h.waitStatus != 0)
    14. // 唤醒排队的线程
    15. unparkSuccessor(h);
    16. return true;
    17. }
    18. return false;
    19. }
    20. // ReentrantLock释放锁资源操作
    21. protected final boolean tryRelease(int releases) {
    22. // 拿到state - 1(并没有赋值给state)
    23. int c = getState() - releases;
    24. // 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
    25. if (Thread.currentThread() != getExclusiveOwnerThread())
    26. throw new IllegalMonitorStateException();
    27. // free,代表当前锁资源是否释放干净了。
    28. boolean free = false;
    29. if (c == 0) {
    30. // 如果state - 1后的值为0,代表释放干净了。
    31. free = true;
    32. // 将持有锁的线程置位null
    33. setExclusiveOwnerThread(null);
    34. }
    35. // 将c设置给state
    36. setState(c);
    37. // 锁资源释放干净返回true,否则返回false
    38. return free;
    39. }
    40. // 唤醒后面排队的Node
    41. private void unparkSuccessor(Node node) {
    42. // 拿到头节点状态
    43. int ws = node.waitStatus;
    44. if (ws < 0)
    45. // 先基于CAS,将节点状态从-1,改为0
    46. compareAndSetWaitStatus(node, ws, 0);
    47. // 拿到头节点的后续节点。
    48. Node s = node.next;
    49. // 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
    50. if (s == null || s.waitStatus > 0) {
    51. s = null;
    52. // 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
    53. for (Node t = tail; t != null && t != node; t = t.prev)
    54. // 从后往前找到状态小于等于0的节点
    55. // 找到离head最新的有效节点,并赋值给s
    56. if (t.waitStatus <= 0)
    57. s = t;
    58. }
    59. // 只要找到了这个需要被唤醒的节点,执行unpark唤醒
    60. if (s != null)
    61. LockSupport.unpark(s.thread);
    62. }

    3 AQS常见的问题

    3.1 AQS中为什么要有一个虚拟的head节点

            因为AQS提供了ReentrantLock的基本实现,而在ReentrantLock释放锁资源时,需要去考虑是否需要执行unparkSuccessor方法,去唤醒后继节点。
            因为Node中存在waitStatus的状态,默认情况下状态为0,如果当前节点的后继节点线程挂起了,那么就将当前节点的状态设置为-1。这个-1状态的出现是为了避免重复唤醒或者释放资源的问题。
            因为AQS中排队的Node中的线程如果挂起了,是无法自动唤醒的。需要释放锁或者释放资源后,再被释放的线程去唤醒挂起的线程。 因为唤醒节点需要从整个AQS双向链表中找到离head最近的有效节点去唤醒。而这个找离head最近的Node可能需要遍历整个双向链表。如果AQS中,没有挂起的线程,代表不需要去遍历AQS双向链表去找离head最近的有效节点。为了避免出现不必要的循环链表操作,提供了一个-1的状态。如果只有一个Node进入到AQS中排队,所以发现如果是第一个Node进来,他必须先初始化一个虚拟的head节点作为头,来监控后继节点中是否有挂起的线程。

    3. 2 AQS中为什么选择使用双向链表,而不是单向链表

            首先AQS中一般是存放没有获取到资源的Node,而在竞争锁资源时,ReentrantLock提供了一个方法,lockInterruptibly方法,也就是线程在竞争锁资源的排队途中,允许中断。中断后会执行cancelAcquire方法,从而将当前节点状态置位1,并且从AQS队列中移除掉。如果采用单向链表,当前节点只能按到后继或者前继节点,这样是无法将前继节点指向后继节点的,需要遍历整个
    AQS从头或者从尾去找。单向链表在移除AQS中排队的Node时,成本很高。
            当前在唤醒后继节点时,如果是单向链表也会出问题,因为节点插入方式的问题,导致只能单向的去找有效节点去唤醒,从而造成很多次无效的遍历操作,如果是双向链表就可以解决这个问题。

  • 相关阅读:
    解锁IT能力:JVS快速开发平台引领企业数字化未来
    「Java开发指南」MyEclipse如何支持Spring Scaffolding?(三)
    用户眼中的 tensor、TensorFlow 系统中的 tensor、tensor 高阶用法 DLPack
    基于树莓派的安保巡逻机器人--(一、快速人脸录入与精准人脸识别)
    NLP中两个词向量间余弦相似度的求解方式
    android studio新版本gradle Tasks找不到assemble
    jvm直接内存相关文档
    读《Complementary Pseudo Multimodal Feature for Point Cloud Anomaly Detection》
    java用位运算实现加减乘除
    2022年推荐算法效率开发必备工具榜单
  • 原文地址:https://blog.csdn.net/qq_45309297/article/details/134488081