深入源码解析 ReentrantLock、AQS:掌握 Java 并发编程关键技术(三)

简介: 深入源码解析 ReentrantLock、AQS:掌握 Java 并发编程关键技术(三)

ReentrantLock.Sync#tryRelease

该方法也体现了锁重入次数的操作,源代码如下:

protected final boolean tryRelease(int releases) {
  // 当前锁线程重入次数减去要释放的次数
    int c = getState() - releases;
    // 当前线程不等于锁持有线程,则判断中断监听锁异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 若减去后的锁次数为 0
    if (c == 0) {
      // 返回 true、设置锁持有线程为 null,其他线程就可以竞争锁了
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 递减锁重入次数,返回 false,锁仍然被当前线程所持有
    setState(c);
    return free;
}

ReentrantLock.Sync#tryRelease 执行流程主要分析如下:

  1. 通过将 AQS#state 属性值减少传入的参数值(参数:1)若减去的结果状态值为 0,就将排它锁 Owner 持有线程设置为 null,同时返回 true,以便于其他的线程有机会执行竞争锁操作
  2. 若减去的结果状态值不为 0,返回 free 变量默认值 false,当前线程仍然继续持有这把锁,其他线程暂时不可以争抢锁

在排它锁中,加锁时 state 状态会增加 1,在解锁时会减去 1,同一把锁,在被重入时,可能会被叠加为 2、3、4 等,只有当调用 unlock 方法次数与调用 lock 方法次数相对应,才会把锁 Owner 持有线程设置为空,也只有这种情况下该方法执行结果才有返回 true

AQS#unparkSuccessor

当 ReentrantLock.Sync#tryRelease 方法执行完以后,会取同步等待队列中首节点,唤醒队列中下一个节点去争抢这把锁,该方法源码如下:

private void unparkSuccessor(Node node) {
    // 获取传入节点的 waitStatus 属性值  
    int ws = node.waitStatus;
    if (ws < 0)
      // 小于 0 通过 CAS 将其修改为 0 
        compareAndSetWaitStatus(node, ws, 0);
    // 获取传入节点的后继节点
    Node s = node.next;
    // 若后继节点为空或者 waitStatus 大于 0 说明它是 CANCELLED-结束状态
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从尾部节点开始扫描,找到距离当前传入节点最近的一个 waitStatus 小于等于 0 的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 将同步等待队列中 > 最前面的一个非 CANCELLED 状态的 Node 线程进行唤醒 
    if (s != null)
        LockSupport.unpark(s.thread);

分析一下此方法分别会作那些事情,如下:

  1. 获取当前传入节点 Node waitStatus 属性值,若它小于 0 时,先通过 CAS 操作将其修改为 0

当前节点其实就是 head 头节点,唤醒的操作不会唤醒头节点,只会唤醒头节点后面不为 CANCELLED 状态的首节点 Node 线程

  1. 获取当前节点的 next 后继节点,若后继节点为空或 waitStatus 大于 0(CANCELLED)那么就会遍历该同步等待队列,从尾部往前查找的方式,匹配到与当前节点最近的一个非 CANCELLED 节点,将其设置为待唤醒的节点
  2. 若待唤醒的节点不为空,调用原生锁 LockSupport#unpark 方法将其唤醒,以便它可以再次去争抢锁

当节点被唤醒后,比如:Thread-A 释放锁成功以后,会调用 AQS#unparkSuccessor 方法唤醒它的下一个节点 Thread-B 所持有的(非 CANCELLED)
随机 Thread-B 被唤醒,它会继续执行 AQS#acquireQueued 方法中的循环,执行:if (p == head && tryAcquire(arg)) 代码块,所以后续被唤醒的线程都会是这样,通过该代码来确保同步队列中的节点能够获取锁资源

那么为什么在释放锁的时候一定要从尾部开始扫描呢?

回顾一下 AQS#enq 方法执行的逻辑,插入新节点时,它是从队列尾部进行节点入队的,看下图红色所标注的,在 CAS 操作成功之后,t.next = node; 操作之前,可能会存在其他线程调用 unlock 方法从 head 开始向后遍历,由于 t.next = node; 还未执行也就意味着同步等待队列关系还未建立完整,就会导致遍历到原始的尾部节点时被中断 > 队列中的链表关系断链了;所以说,从后往前遍历就不会出现这个问题

挂起线程被唤醒后执行过程

当持有锁的线程调用 ReentrantLock#unlock 方法,原本被挂起的 Thread-B、Thread-C 线程就有机会被唤醒再继续执行,被唤醒之后的线程会继续执行 AQS#acquireQueued 方法内的循环,该方法在上面已经分析过了,接下来以 Thread-B 被唤醒后为例,看它整个的执行过程以及变化,以流程图的方式呈现

同步等待队列变更结构图:

同步等待队列执行过程流程图:

博主是以如下源码,对 ReentrantLock、AQS 核心方法源码进行查看的,分享如下:

public class MultiThreadReentrantLockDemo {
    private static final ReentrantLock LOCK = new ReentrantLock();
    public void threadAProcess() {
        LOCK.lock();
        try {
            System.out.println("执行:threadAProcess 方法");
            // 处理业务逻辑中....
            // 断点过程中该时间可以延长
            TimeUnit.SECONDS.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            LOCK.unlock();
        }
    }
    public void threadBProcess() {
        LOCK.lock();
        try {
            System.out.println("执行:threadBProcess 方法");
            // 处理业务逻辑中....
            TimeUnit.SECONDS.sleep(60 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            LOCK.unlock();
        }
    }
    public void threadCProcess() {
        LOCK.lock();
        try {
            System.out.println("执行:threadCProcess 方法");
            // 处理业务逻辑中....
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            LOCK.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MultiThreadReentrantLockDemo multiThreadLock = new MultiThreadReentrantLockDemo();
        new Thread(()-> multiThreadLock.threadAProcess(), "Thread-A").start();
        Thread threadB = new Thread(() -> multiThreadLock.threadBProcess(), "Thread-B");
        threadB.start();
        // 可能会出现 Thread-C 先执行的情况,所以先通过 join 方法让线程 B 先跑完
        threadB.join();
        new Thread(()-> multiThreadLock.threadCProcess(), "Thread-C").start();
    }
}

注意:断点模式下,要以 Thread 模式执行;如下图:

公平锁、非公平锁区别

锁的公平与否其实取决于获取锁的顺序性,若为公平锁,那么获取锁的顺序应该绝对符合 FIFO 队列 > 先进先出的特性,上面所分析的例子都是以非公平锁(默认是非公平锁)只要 CAS 设置 AQS#state 属性值成功,就代表当前线程获取到了锁,而公平锁不一样,差异的地方有如下两点:

1、FairSync#lock、NonfairSync#lock

非公平锁在获取锁时,先通过 CAS 操作进行锁抢占,而公平锁不会

2、FairSync#tryAcquire、NonfairSync#tryAcquire

两者方法之间不同之处在于判断多了一个条件:hasQueuedPredecessors,也就是说加入了同步队列中当前节点是否有前驱节点的判断,若该方法返回 true,则表示有线程比当前线程更早入队、更早地请求获取锁,因此,需要等待前驱节点的线程获取完再释放锁以后才能继续获取锁!

public final boolean hasQueuedPredecessors() {
    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());
}

1、h != t:头尾节点是否相同,若相同则表示队列中只有一个节点,即当前未发生锁竞争

假设:当前只有线程 Thread-A 一人争抢锁,那么 head == null && tail ==null,那么返回 false 会去争抢锁;反之,会继续走第二步的判断

2、(s = h.next) == null:检查头节点的后继节点是否为空,即判断是否存在后继节点

假设:线程 Thread-A、Thread-B 同时争抢锁,Thread-A 抢到了,那么同步等待队列中会有头节点、Thread-B 所在节点,条件不满足返回 false,会继续走第三步的判断;反之,当前只有一个节点 > 返回 true 不会去争抢锁,不走第三步的判断

3、s.thread != Thread.currentThread():检查后继节点的线程是否与当前线程不同,即判断后继节点持有线程是否为当前线程

假设:头节点的后继节点持有线程就是当前的线程,会返回 false 会去争抢锁;反之,头节点的后继节点持有线程不是当前的线程,会返回 true 不会去争抢锁,它会进入到排队模式!!!

总结

ReentrantLock 基于悲观锁实现(LockSupport),但是在处理 AQS#state 锁状态时是基于 CAS 乐观锁实现的,两者在不同场景下都会各自的好处,因为前者已经悲观锁,后者再用 CAS 操作并没有任何问题

>在这里其实就是偷换概念了,不一定用了悲观锁就不能用乐观锁

该篇博文介绍了 JUC 组件下 ReentrantLock 核心概念、使用、源码以及 AQS 基础组件的核心方法,阐述了 AQS 内部实现、数据结构以及节点变更过程,在后面,看 ReentrantLock 是如何基于 AQS 核心方法去完成其内部锁竞争工作的、锁释放后如何唤醒其他节点线程,全文上下以画图+文字加以说明,不限于时序图、结构图、流程图,最后说明了在 ReentrantLock 公平锁、非公平锁之间的区别,希望能够帮助你快速理解 AQS 内部如何巧妙处理高并发场景问题的

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

目录
相关文章
|
3月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
243 1
|
3月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
188 6
|
3月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
181 0
|
4月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
796 1
|
4月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
485 100
|
4月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
4月前
|
NoSQL Java 关系型数据库
超全 Java 学习路线,帮你系统掌握编程的超详细 Java 学习路线
本文为超全Java学习路线,涵盖基础语法、面向对象编程、数据结构与算法、多线程、JVM原理、主流框架(如Spring Boot)、数据库(MySQL、Redis)及项目实战等内容,助力从零基础到企业级开发高手的进阶之路。
370 1
|
4月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
301 16
|
5月前
|
安全 Java Shell
Java模块化编程(JPMS)简介与实践
本文全面解析Java 9模块化系统(JPMS),帮助开发者解决JAR地狱、类路径冲突等常见问题,提升代码的封装性、性能与可维护性。内容涵盖模块化核心概念、module-info语法、模块声明、实战迁移、多模块项目构建、高级特性及最佳实践,同时提供常见问题和面试高频题解析,助你掌握Java模块化编程精髓,打造更健壮的应用。
|
5月前
|
安全 算法 Java
Java泛型编程:类型安全与擦除机制
Java泛型详解:从基础语法到类型擦除机制,深入解析通配符与PECS原则,探讨运行时类型获取技巧及最佳实践,助你掌握泛型精髓,写出更安全、灵活的代码。

推荐镜像

更多
  • DNS