深入源码解析 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,订阅一波不再迷路

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

目录
相关文章
|
2月前
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
306 44
|
2月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
337 1
|
3月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
119 4
|
3月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
764 1
|
3月前
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
578 4
|
2月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
240 0
|
3月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
212 3
|
3月前
|
存储 人工智能 算法
Java 大视界 -- Java 大数据在智能医疗影像数据压缩与传输优化中的技术应用(227)
本文探讨 Java 大数据在智能医疗影像压缩与传输中的关键技术应用,分析其如何解决医疗影像数据存储、传输与压缩三大难题,并结合实际案例展示技术落地效果。
|
3月前
|
机器学习/深度学习 算法 Java
Java 大视界 -- Java 大数据在智能物流运输车辆智能调度与路径优化中的技术实现(218)
本文深入探讨了Java大数据技术在智能物流运输中车辆调度与路径优化的应用。通过遗传算法实现车辆资源的智能调度,结合实时路况数据和强化学习算法进行动态路径优化,有效提升了物流效率与客户满意度。以京东物流和顺丰速运的实际案例为支撑,展示了Java大数据在解决行业痛点问题中的强大能力,为物流行业的智能化转型提供了切实可行的技术方案。
|
3月前
|
分布式计算 Cloud Native 安全
Java:历久弥新的技术基石
Java:历久弥新的技术基石

推荐镜像

更多
  • DNS