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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 深入源码解析 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天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
14 2
|
3天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
11天前
|
JSON 前端开发 JavaScript
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
24 1
|
12天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
33 2
|
18天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
28 7
|
20天前
|
移动开发 前端开发 Java
过时的Java技术盘点:避免在这些领域浪费时间
【10月更文挑战第14天】 在快速发展的Java生态系统中,新技术层出不穷,而一些旧技术则逐渐被淘汰。对于Java开发者来说,了解哪些技术已经过时是至关重要的,这可以帮助他们避免在这些领域浪费时间,并将精力集中在更有前景的技术上。本文将盘点一些已经或即将被淘汰的Java技术,为开发者提供指导。
49 7
|
16天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
36 3
|
16天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
30 3
|
16天前
|
SQL 监控 Java
Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面
本文探讨了Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,以实现高效稳定的数据库访问。示例代码展示了如何使用HikariCP连接池。
10 2
|
18天前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
25 4

推荐镜像

更多