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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入源码解析 ReentrantLock、AQS:掌握 Java 并发编程关键技术(二)

ReentrantLock.NonfairSync#tryAcquire

NonfairSync#tryAcquire 方法重写至 AQS 类,AQS 该方法并没有实现,而是抛出异常,具体的实现内容交由给子类去进行实现,这里采用了设计模式 > 模版方法

具体的子类实现:ReentrantLock.NonfairSync#tryAcquire,该方法作用:尝试获取一把锁,若成功返回 true、失败返回 false

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
  // 获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();// 获取 state 状态
    if (c == 0) { // 代表无锁状态
      // CAS 替换 state 值,CAS 成功表示锁获取成功
        if (compareAndSetState(0, acquires)) {
          // 保存当前获取锁的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 若同一个线程多次获取同一把锁,直接增加锁重入次数即可
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  1. 获取当前内存中 state 锁状态值
  2. state 状态为 0 代表当前锁处于无锁状态,首次获取锁的线程可以通过 CAS 操作更新 state 锁状态值
  3. 若当前线程等于锁占有的线程,则增加锁重入次数即可
  4. 其他情况,代表获取锁失败的线程,执行 AQS#acquire 方法中的 addWaiter(Node.EXCLUSIVE), arg) 方法 > 添加独占模式的 Node 队列节点

AQS#addWaiter

当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node,源码如下:

private Node addWaiter(Node mode) {
  // 将当前线程封装为 Node 节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // tail 指向 AQS 同步队列中的尾部节点,默认:null
    Node pred = tail;
    // tail 不为空的情况下,说明同步队列中存在节点
    if (pred != null) {
      // 将当前线程 Node prev 前驱节点指针指向原来的尾部节点
        node.prev = pred;
        // 通过 CAS 操作将当前 Node 设置为尾部节点
        if (compareAndSetTail(pred, node)) {
          // 设置成功以后,将原来的尾部节点 next 后继节点指向当前 Node
            pred.next = node;
            return node;
        }
    }
    // tail 为空的情况下,调用 enq 方法将当前 Node 添加到同步队列中
    enq(node);
    return node;
}

入参:mode 表示节点的状态,ReentrantLock 传入的状态参数:Node.EXCLUSIVE 代表独占模式,意味着重入锁获取锁采用独占的方式,addWaiter 方法基本的执行过程,如下所示:

  1. 将当前线程封装为 Node 节点对象
  2. 判断当前同步队列中的尾部节点是否为空
  3. 若尾部节点不为空,通过 CAS 操作将当前线程的 Node 添加到同步队列中,并将新加入的 Node 设置为尾节点,采用尾插法的方式进行队列入队的
  4. 若尾部节点为空或者 CAS 设置尾部节点失败,调用 enq 方法将当前 Node 添加到同步队列中

AQS#enq

该方法通过自旋的方式以便可以成功将当前节点加入到同步队列中

/**
 *      +------+  prev +-----+       +-----+
 * head |      | <---- |     | <---- |     |  tail
 *      +------+       +-----+       +-----+
 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 尾节点为空,说明当前同步队列中未存在元素
        // 初始化一个空对象 Node,先通过 CAS 将其设置为头节点,若成功再将其设置为尾节点
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
          // 当前节点的前驱节点指向原来尾部节点、将当前节点设置为尾部节点、原来尾部节点后继节点指向当前节点
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq 方法执行只是为了维护同步等待队列的节点元素,当多个线程开始竞争锁时,必然会进行排队,第一次入队的线程不仅要承担将自身加入到队列中,同时还需要初始化一个空 Node 对象,将其设置为头尾节点

图解分析

假设有 3 个线程同时来争抢锁,那么截止到 AQS#addWaiter 或 AQS#enq 方法结束之后,AQS 中同步等待队列结构图,如下所示:

AQS#acquireQueued

当执行完 AQS#addWaiter 方法以后,会将返回的 Node 参数传递给 acquireQueued 方法,去实现锁竞争、阻塞线程逻辑,方法源码如下:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
          // 获取当前 Node 节点的前驱 prev 节点
            final Node p = node.predecessor();
            // 若前驱 prev 节点为头节点,当前 Node 重新尝试获取锁
            if (p == head && tryAcquire(arg)) {
              // 获取锁成功,说明锁已经被持有的线程所释放,设置当前 Node 为头节点
                setHead(node);
                // 将原 head 头节点从同步队列中移除
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 获取锁失败,会获取前驱节点并更新 waitStatus 状态值
            // 随机调用原生锁 LockSupport#park 方法阻塞当前竞争锁的线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued 方法基本的执行过程同时以 AQS#enq 分析中的图解分析为例,如下所示:

  1. 获取当前节点 Node prev 前驱节点

比如:当前是线程 B,那么它的前驱节点就是 Thread 为 null 的头节点

  1. 若 prev 前驱节点为 head 头节点,那么它有资格再去争抢一次锁,调用 ReentrantLock.NonfairSync#tryAcquire 方法抢占锁

也就是说线程 B 在这里也会有一次机会再去争夺锁

  1. 若抢占锁成功,把当前抢到锁的 Node 节点设置为 head 头节点,并且移除原有的头节点
  2. 若抢占锁失败,先通过 shouldParkAfterFailedAcquire 方法更新一次 waitStatus 值状态,然后再调用原生锁支持 > LockSupport.park(this) 阻塞当前线程等待后续被唤醒

仍然以线程 A 未释放锁,线程 B 处于首节点的情况作以说明:由于 acquireQueued 方法是死循环,所有的 Node 新建时 waitStatus 属性值都为 0 (除了 Condition 条件变量)第一次遍历时会抢一次锁;这一次会调用 shouldParkAfterFailedAcquire 方法将 waitStatus -> 0 更改为 SIGNAL-待唤醒状态,该方法执行完以后会返回 false,然后会继续第二次循环,第二次执行 shouldParkAfterFailedAcquire 方法返回 true,接着会调用 parkAndCheckInterrupt 方法使用原生锁方式:LockSupport.park(this) > 阻塞当前线程并返回当前线程是否中断的标识

  1. 最后,通过 cancelAcquire 方法取消当前线程获取锁的节点

AQS#shouldParkAfterFailedAcquire

线程 A 未释放锁,线程 B、线程 C 来争抢锁肯定会失败,失败以后会调用 shouldParkAfterFailedAcquire 方法,Node#waitStatus 存在五种状态,如下:

  1. CANCELLED:值为 1,即为结束状态,在同步等待队列中等待的线程超时或被中断,需要从同步队列中取消该线程 Node 节点,进入该状态以后的节点将不再发生变化
  2. 0:初始化状态
  3. SIGNAL:值为 -1,当前驱节点释放锁以后,就会通知标识为 SIGNAL 状态的后继 Node 节点线程
  4. CONDITION:值为 -2,与 ReentrantLock#newCondition 条件变量有关系,AQS.ConditionObject#addConditionWaiter 在该方法中会提现出来
  5. PROPAGATE:值为 -3,在共享模式下,PROPAGATE 处于可运行状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   int ws = pred.waitStatus;
   // 若前驱节点为 SIGNAL,意味着只需要等待其他前驱节点的线程被释放
   // 当获取锁的线程调用 release 方法后,该前驱节点的线程就会被唤醒
   if (ws == Node.SIGNAL)
       // 返回 true,意味着当前线程可以放心调用 parkAndCheckInterrupt 方法进行挂起
       return true;
   // waitState 大于 0,意味着 prev 前驱节点取消了排队操作,直接将这个节点移除即可
   if (ws > 0) {
       // 相当于:pred=pred.prev;node.prev=pred;
       // 从尾部节点开始查找,直到将所有的 CANCELLED 节点移除
       do {
           node.prev = pred = pred.prev;
       } while (pred.waitStatus > 0);
       pred.next = node;
   } else {
       // 使用 CAS 设置前驱 prev 节点状态为 SIGNAL
       compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
   }
   return false;
}

该方法主要作用:通过 Node 节点状态来判断,线程 B、线程 C 竞争锁失败后是否应该要被挂起

  1. 若 Thread-B、Thread-C 前驱 prev 节点状态为 SIGNAL,表示可以放心挂起所在的当前线程
  2. 若当前线程 prev 节点状态为 CANCELLED,采用循环方式扫描同步等待队列将 CANCELLED 状态的节点从同步等待队列中移除
  3. 以上两个条件都满足,将前驱 prev 节点状态改为 SIGNAL,返回 false

该方法返回 true、false 代表的含义不同,当返回 true 时,不会进入 AQS#acquireQueued 方法的下一次循环,会调用 parkAndCheckInterrupt 方法将当前线程阻塞;当返回 false 时,会进入到 AQS#acquireQueued 方法的下一次循环再次尝试争抢一次锁,当抢锁成功当前线程就是独占线程,抢锁失败再调用 parkAndCheckInterrupt 方法将当前线程阻塞

AQS#parkAndCheckInterrupt

parkAndCheckInterrupt 方法逻辑比较简单,先看源码,如下:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
  1. 调用 LockSupport#park 方法挂起当前线程编程 waiting 状态

LockSupport 原生锁支持

park 方法:阻塞当前线程,不需要使用 sync 修饰,直接可以使用

unpark 方法:唤醒指定线程

unpark 方法可以先于 park 方法先调用,unpark 相当于是获取许可数量 1、park 相当于是消费许可数量 1

  1. Thread#interrupted:返回当前线程是否被其他线程触发过中断请求,也就是调用 Thread#interrupt 方法;若有触发过中断请求,那么该方法会返回当前的中断标识为 true,并且会对中断标识进行复位标识已经响应过了中断请求,也就是会在 AQS#acquire 方法中执行 selfInterrupt 方法
  2. selfInterrupt:标识当前线程是否执行 AQS#acquireQueued 方法时被中断过,若被中断过,则需要响应中断请求,因为在线程调用 AQS#acquireQueued 方法是不会去响应中断请求的

通过 AQS#acquireQueued 方法来竞争锁,若 Thread-A 仍然还在执行中未释放锁,那么 Thread-B、Thread-C 还会继续挂起

到这里,锁相关的竞争方法在这里基本上都介绍过了,其实看到这里,能发现,当竞争的锁线程失败时,会调用 LockSupport#park 方法阻塞住,等待锁匙放时,还会有 LockSuppor#unpark 方法进行锁匙放,下面就来分析锁匙放时的一些核心方法是如何处理的!!!

锁释放核心方法

若此时 Thread-A 释放锁了,那么接下来 Thread-B、Thread-C 是如何走的呢?

ReentrantLock#unlock

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

在 unlock 方法中,会调用其内部类 Sync#release 方法,但由于 Sync 并未其父类 AQS#release 方法,所以它会延用其父类 AQS#release 方法的处理逻辑,源码如下:

public final boolean release(int arg) {
  // 若释放占用当前锁的节点 Node 线程成功
    if (tryRelease(arg)) {
        // 获取 AQS 同步等待队列中的 head 头节点
        Node h = head;
        // 若 head 节点不为空 & waitStatus 非默认值,直接唤醒下一个节点去争抢锁
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

该方法主要的执行流程分为几步,如下:

  1. 先调用 ReentrantLock.Sync#tryRelease 方法探测锁释放是否可以成功,它来自 AQS 子类 ReentrantLock.Sync 所实现的
  2. 获取同步等待队列中的 head 首节点,若其不为空,并且它的 waitStatus 属性值非默认值 0,那么就会调用 unparkSuccessor 方法唤醒队列中的下一个节点
目录
相关文章
|
11天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
10天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
10天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
10天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
29天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
11天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
3月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
87 0
|
3月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
68 0
|
3月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
73 0

热门文章

最新文章

推荐镜像

更多