当前位置: 首页 > news >正文

并发编程——07 深入理解AQS之独占锁ReentrantLock源码分析

1 管程——Java同步的设计思想

1.1 简介

  • 管程是一套管理共享资源 + 控制线程并发的设计思想 ,核心解决 2 件事:

    • 互斥:同一时间,只能让 1 个线程访问共享资源(比如多个线程抢着操作同一个变量,得排队);

    • 同步:线程之间得“商量着来”(比如线程 A 要等线程 B 做完某件事,再继续执行);

  • 管程发展出 3 种模型(Hasen、Hoare、MESA),但实际开发里 MESA 是 Java 等语言的底层逻辑

    在这里插入图片描述

    • 入口队列→ 解决互斥

      • 多个线程想访问管程里的共享资源时,得先进入入口队列排队;

      • 管程会保证:同一时间,只有 1 个线程能出队列,真正去操作共享资源 → 这就实现了互斥,避免多线程乱抢共享资源;

    • 条件变量 + 等待队列→ 解决同步

      • 线程执行时,可能遇到需要等待某个条件(比如“数据还没准备好”“资源没释放”),这时候线程会:
        • 调用 wait() → 自己进入条件变量对应的等待队列 ,并让出管程的访问权,让其他线程能访问管程;
        • 等条件满足时,其他线程调用 notify() / notifyAll() → 唤醒等待队列里的线程,让它们重新去入口队列排队,争取执行权 → 这就实现了线程之间的同步协作;
    • 共享资源 + 操作方法 → 管程的核心资产

      • 共享变量:管程要保护的资源(比如多个线程要操作的同一个计数器、同一个对象);

      • 方法(如方法 X、Y):线程实际执行的逻辑,里面会操作共享变量,也会用 wait() / notify() 控制同步;

  • MESA 最关键的特点是:线程被 notify() 唤醒后,不会直接抢占执行权,而是重新回到入口队列排队

1.2 Java 管程的两种实现方式

  • Java 里管程思想通过 两种底层机制落地

    • 基于Object的 Monitor 机制(和synchronized绑定)

      • 隐式、简单:用synchronized修饰方法/代码块时,JVM 自动用 Monitor 控制互斥和同步(靠wait()/notify()通信);
      • 缺点:功能少(只有 1 个等待队列),不够灵活;
    • 基于 AQS(抽象队列同步器)java.util.concurrent.locks 包里的 Lock 体系)

      • 显式、灵活:像ReentrantLock就是 AQS 实现,能创建 多个 Condition(条件变量),每个Condition对应独立等待队列,精准控制线程协作;
  • 例:

    @Slf4j
    public class ConditionDemo2 {// ReentrantLock:显式锁,实现互斥(控制线程排队访问共享资源)private static final ReentrantLock lock = new ReentrantLock();// Condition:基于 AQS 的条件变量,每个 Condition 对应一个等待队列 ,让线程按需等待/唤醒private static final Condition condition = lock.newCondition();public static void main(String[] args) throws InterruptedException {new Thread(() -> {log.debug("t1开始执行....");lock.lock(); // 1. 加锁:抢占“访问权”,保证同一时间只有1个线程执行临界区try {log.debug("t1获取锁....");condition.await(); // 2. 等待:释放锁,进入 condition 的等待队列,阻塞自己} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); // 3. 解锁:不管是否异常,都要释放锁,让其他线程能抢到log.debug("t1执行完成....");}}, "t1").start();new Thread(() -> {log.debug("t2开始执行....");lock.lock();try {log.debug("t2获取锁....");// 让线程在obj上一直等待下去condition.await();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();log.debug("t2执行完成....");}}, "t2").start();// 主线程先睡 2 秒,等 t1、t2 进入等待队列Thread.sleep(2000);log.debug("准备获取锁,去唤醒 condition上阻塞的线程");lock.lock(); // 先抢锁,才能操作 conditiontry {condition.signalAll(); // 唤醒 condition 等待队列里的所有线程(t1、t2)log.debug("唤醒condition上阻塞的线程");} catch (Exception e) {e.printStackTrace();} finally {lock.unlock(); // 释放锁}}
    }
    
    线程t1线程t2主线程start() → 执行 lock.lock() 抢锁start() → 执行 lock.lock() 抢锁(t1 已抢到,t2 阻塞在锁竞争)执行 condition.await() → 释放锁,进入 condition 等待队列抢到锁(t1 释放后)→ 执行 condition.await() → 释放锁,进入 condition 等待队列执行 Thread.sleep(2000) → 等待 2 秒执行 lock.lock() 抢锁(此时 t1、t2 都已释放锁,能抢到)执行 condition.signalAll() → 唤醒 t1、t2(从 condition 等待队列移到 锁竞争队列)执行 lock.unlock() → 释放锁重新抢锁(成功后)→ 继续执行 await() 之后的逻辑 → 最终 unlock()重新抢锁(成功后)→ 继续执行 await() 之后的逻辑 → 最终 unlock()线程t1线程t2主线程
  • 核心对比:synchronized vs ReentrantLock + Condition

    对比项synchronized(Monitor 机制)ReentrantLock + Condition(AQS 机制)
    等待队列数量只有 1 个公共等待队列可创建 多个 Condition,每个对应独立队列
    唤醒精度只能 notify()(随机唤醒)/ notifyAll()可精准唤醒某个 Condition 队列的线程(signal() / signalAll()
    使用复杂度简单(隐式加锁/释放)复杂(需手动 lock() / unlock(),但更灵活)

2 AQS原理分析

2.1 简介

  • AQS 是 Java 并发包(java.util.concurrent)里的同步器框架 ,把“锁、同步工具(比如 CountDownLatch、Semaphore)”的通用逻辑(比如“排队等待”“抢锁”)抽象出来,让开发者不用重复写底层同步代码;

    • 打个比方:当想编写代码实现“锁”“计数器”这些同步工具的时候,不用自己实现“线程怎么排队”“怎么抢资源”等等,直接基于 AQS 改一改就行 → AQS 是这些同步工具的底层模板
  • AQS 的核心作用:统一“同步行为”。Java 并发包里的同步工具(LockCountDownLatchSemaphore 等),看起来功能不同,但底层都要解决线程怎么协调访问共享资源 的问题。AQS 把这些通用逻辑(比如下面这些)抽象成框架:

    • 等待队列:线程抢不到资源,得排队等待;

    • 条件队列:线程需要“等某个条件满足”(比如 CountDownLatch 要等计数器归 0),得进条件队列等通知;

    • 独占/共享获取:有些资源只能“一个线程用”(比如 ReentrantLock 的独占锁),有些可以“多个线程一起用”(比如 Semaphore 的共享锁);

  • 想基于 AQS 做一个同步器(比如自己写个锁),步骤如下:

    1. 写一个内部类 Sync,继承 AQS:比如 ReentrantLock 里的 FairSync(公平锁)、NonfairSync(非公平锁),都是继承 AQS 的内部类;

    2. 把同步器的方法,映射到 Sync 的方法:比如你写了个自定义锁,调用 lock() 时,实际是让 Sync 里的 acquire() 方法去抢资源;调用 unlock() 时,让 release() 方法释放资源 → AQS 已经帮你把“抢资源、排队、唤醒”这些逻辑写好了,你只需填业务细节

  • AQS 内置了这些“通用同步逻辑”,你基于它做的同步器,自动拥有这些特性:

    • 阻塞等待队列:线程抢不到资源,会被放进一个 CLH 队列(一种高效的等待队列)里阻塞,避免空转浪费 CPU;

    • 共享/独占模式

      • 独占:比如 ReentrantLock 的独占锁,同一时间只能一个线程用;
      • 共享:比如 Semaphore 允许多个线程同时获取许可;
    • 公平/非公平策·略

      • 公平:严格按“线程排队顺序”抢资源(比如 ReentrantLock 的公平锁);
      • 非公平:新来的线程可以直接“插队”抢资源(默认更高效,比如 ReentrantLock 的非公平锁);
    • 可重入:线程拿到锁后,能多次进入(比如 ReentrantLock 支持重入,避免自己锁自己);

    • 允许中断:线程等待时,支持被中断(比如 lockInterruptibly() 方法);

  • 你常用的并发工具,几乎都基于 AQS:

    • ReentrantLock(锁)→ 用 AQS 实现“独占、重入、公平/非公平”;

    • CountDownLatch(计数器)→ 用 AQS 实现“线程等待计数器归 0”;

    • Semaphore(信号量)→ 用 AQS 实现“多线程共享资源访问”。

2.2 核心结构

  • 核心变量:

    private volatile int state;
    
    • AQS 用state表示共享资源的可用状态 (比如锁是否被占用、剩余许可数量等);

      • 例子:ReentrantLock 里,state=0 表示锁空闲;state>0 表示锁被占用(值也代表“重入次数”);
      • 例子:Semaphore 里,state 直接表示“剩余可用许可数”;
    • volatile 关键字:保证 state 的修改对所有线程“可见”(避免多线程下的“脏读”问题);

  • AQS 给 state 提供了 3 种访问方式:

    protected final int getState() {return state;
    }
    
    • 简单读 state 的值,因为 statevolatile,读操作本身是线程安全的;
    protected final void setState(int newState) {state = newState;
    }
    
    • 直接写 state,看似普通,但要注意:只有当前线程已独占资源时,才能安全调用 (比如 ReentrantLock 拿到锁的线程,才能直接改 state);
    protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
    • CAS 操作(底层靠 Unsafe 类实现),核心逻辑:如果当前state的值等于expect(期望值),就把它改成update,返回成功;否则不修改,返回失败;
  • AQS 支持 两种资源抢占方式,应对不同同步需求:

    • 独占模式(Exclusive)
      • 特点:同一时间,只有 1 个线程能拿到资源(比如 ReentrantLock 的独占锁);
      • 对应方法:tryAcquire(int)(抢资源)、tryRelease(int)(释放资源);
    1. 共享模式(Share)
      • 特点:同一时间,多个线程能拿到资源(比如 Semaphore 允许多个线程拿许可,CountDownLatch 让多个线程等待同一个计数器);
      • 对应方法:tryAcquireShared(int)(抢资源,返回值有特殊含义)、tryReleaseShared(int)(释放资源);
  • AQS 是 抽象类,想基于它实现同步器,必须重写下面这些方法(“钩子方法”):

    方法名作用
    isHeldExclusively()判断当前线程是否“独占”资源(一般 Condition 相关场景用)
    tryAcquire(int)【独占模式】尝试抢资源:成功(返回 true)或失败(返回 false)
    tryRelease(int)【独占模式】尝试释放资源:成功(返回 true)或失败(返回 false)
    tryAcquireShared(int)【共享模式】尝试抢资源:负数 → 失败;0 → 成功,但没剩余资源;正数 → 成功,且有剩余资源
    tryReleaseShared(int)【共享模式】尝试释放资源:成功允许唤醒后续等待线程(返回 true),否则返回 false

2.3 AQS 的两大核心队列

  • AQS 里有两种队列:

    • 同步等待队列(CLH 队列):处理“线程抢资源失败,排队等锁”的场景;

    • 条件等待队列:处理“线程等某个条件满足(比如 Condition.await())”的场景;

  • AQS 的 Node 节点有 5 种状态(用 int 表示),用来标记线程“在队列里是啥情况”:

    状态值含义
    0(初始化状态)节点在同步队列里,等着抢资源(锁)
    CANCELLED(1)线程被取消(比如超时、被中断),节点要被清理出队列
    SIGNAL(-1)节点的后继节点需要被唤醒(当前节点释放资源后,要唤醒下一个节点)
    CONDITION(-2)节点在条件队列里,等着“条件满足”
    PROPAGATE(-3)共享模式下,后续节点需要被唤醒(比如 Semaphore 释放许可时,批量唤醒)

2.3.1 同步等待队列

  • AQS中的同步等待队列也称 CLH 队列,CLH 队列是 Craig、Landin、Hagersten 三人发明的一种基于双向链表实现的 FIFO(先进先出)队列,Java中的 CLH 队列是原 CLH 队列的一个变种,线程由原自旋机制改为阻塞机制;

  • 作用:线程抢资源(比如锁)失败时,被包装成 Node 节点,扔进这个队列里“排队等唤醒”;

  • AQS 依赖 CLH 同步队列来完成同步状态的管理:

    在这里插入图片描述

    • 入队:线程抢资源失败 → 被包装成 Node → 加入 CLH 队列尾部 → 线程阻塞(不占 CPU);
    • 出队:持有资源的线程释放资源 → 唤醒队列头部的线程(公平锁) → 头部线程重新抢资源(成功就出队,继续执行);
  • 举个栗子(比如 ReentrantLock 抢锁):

    • 线程 1 拿到锁 → state 标记为“占用”;

    • 线程 2、3 来抢锁 → 失败 → 被包装成 Node,加入 CLH 队列 → 阻塞;

    • 线程 1 释放锁 → 唤醒队列头部的线程 2 → 线程 2 重新抢锁(成功后 state 标记为“占用”,继续执行)。

2.3.2 条件等待队列

  • 基于单向链表实现,每个Condition(条件变量)对应一个独立的条件队列;

  • 作用:线程执行到condition.await()时,会从同步队列转移到条件队列,等待某个条件触发(比如 condition.signal());

  • 核心流程:

    在这里插入图片描述

    • 入队:线程在同步队列中拿到锁 → 执行 condition.await() → 释放锁 → 从同步队列移除 → 加入条件队列 → 线程阻塞;

    • 出队:其他线程执行 condition.signal() → 条件队列里的线程被转移回同步队列 → 重新排队抢锁 → 抢到锁后继续执行;

  • 举个栗子(比如 ReentrantLock + Condition):

    • 线程 A 拿到锁 → 执行 condition.await() → 释放锁 → 从同步队列移到条件队列 → 阻塞;

    • 线程 B 拿到锁 → 执行 condition.signal() → 线程 A 从条件队列移回同步队列 → 线程 A 重新排队抢锁 → 抢到后继续执行;

2.3.3 两个队列的关系

  • 线程会在 同步队列 ↔ 条件队列 之间转移:
    • condition.await() → 同步队列 → 条件队列(线程等条件);
    • condition.signal() → 条件队列 → 同步队列(条件满足,线程重新抢资源);
  • 核心目的:让线程“按需等待” ,避免无效竞争(比如条件不满足时,没必要占着同步队列的位置)。

2.4 基于AQS实现一把独占锁

/*** 基于 AQS 实现一把独占锁* * 核心思路:* 1. 继承 AbstractQueuedSynchronizer(AQS),复用其队列管理、阻塞唤醒等能力* 2. 重写 tryAcquire/tryRelease 方法,定义「加锁」和「释放锁」的具体规则* 3. 暴露 lock/unlock 等对外 API,让用户可以像使用普通锁一样操作*/
public class TulingLock extends AbstractQueuedSynchronizer {// 1. 重写 AQS 核心方法:定义加锁规则@Overrideprotected boolean tryAcquire(int unused) {// compareAndSetState(0, 1):CAS 操作,尝试把 state 从 0(未占用)改为 1(占用)// state 是 AQS 核心变量,标记锁的状态(0=空闲,1=占用)if (compareAndSetState(0, 1)) {// 如果 CAS 成功,标记当前线程为锁的持有者// setExclusiveOwnerThread 是 AQS 提供的方法,记录「独占模式」下的持有线程setExclusiveOwnerThread(Thread.currentThread());return true; // 加锁成功}return false; // CAS 失败,加锁失败(锁已被占用)}// 2. 重写 AQS 核心方法:定义释放锁规则@Overrideprotected boolean tryRelease(int unused) {// 标记锁的持有者为 null(释放独占线程)setExclusiveOwnerThread(null);// 把 state 置为 0,标记锁回到空闲状态setState(0);return true; // 释放锁成功(AQS 会唤醒等待队列中的线程)}// 3. 暴露对外 API:让用户使用锁/*** 加锁方法:调用 AQS 的 acquire(1)* acquire 是 AQS 提供的模板方法,内部会调用我们重写的 tryAcquire* 如果 tryAcquire 失败,线程会进入同步队列阻塞等待*/public void lock() {acquire(1);}/*** 尝试加锁(非阻塞):直接调用我们重写的 tryAcquire* 成功返回 true,失败返回 false(不会阻塞线程)*/public boolean tryLock() {return tryAcquire(1);}/*** 释放锁方法:调用 AQS 的 release(1)* release 是 AQS 提供的模板方法,内部会调用我们重写的 tryRelease* 释放后会唤醒同步队列中的等待线程*/public void unlock() {release(1);}/*** 辅助方法:判断锁是否被占用* 通过 AQS 的 state 变量判断(state != 0 表示被占用)*/public boolean isLocked() {return getState() != 0;}
}
  • 核心逻辑理解读:

    • 继承 AQSTulingLock extends AbstractQueuedSynchronizer,直接复用 AQS 的「同步队列管理」「线程阻塞/唤醒」能力,不用自己实现队列和阻塞逻辑;

    • tryAcquire:定义加锁规则

      • compareAndSetState(0, 1) 做 CAS 操作,保证原子性抢占锁;
      • 成功后用 setExclusiveOwnerThread 标记当前线程为锁的持有者,实现独占锁(同一时间只有一个线程能持有);
    • tryRelease:定义释放规则

      • 把持有线程置为 nullstate 置为 0,彻底释放锁;
      • AQS 会自动唤醒同步队列中的等待线程,让它们重新竞争锁;
    • 对外 API(lock/unlock 等)

      • 借助 AQS 的模板方法(acquire/release),把 tryAcquire/tryRelease 封装成用户可用的锁操作;
      • TulingLockReentrantLock 一样,支持 lock() 阻塞加锁、tryLock() 非阻塞加锁等;
  • 这段代码本质上就是简易版的 ReentrantLock(不支持重入):

    • ReentrantLock 也基于 AQS 实现,核心逻辑一样;

    • 差异点:ReentrantLock支持可重入(加锁次数和state关联,当前线程再次加锁不会失败),而这段代码是“非重入”的独占锁(同一线程多次 lock() 会阻塞);

    • 如果想支持可重入,只需修改 tryAcquire

      @Override
      protected boolean tryAcquire(int unused) {Thread current = Thread.currentThread();int c = getState();if (c == 0) { // 锁空闲if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) { // 同一线程再次加锁(可重入)setState(c + 1); // state 计数 +1return true;}return false;
      }
      

3 ReentrantLock 源码分析

3.1 简介

  • ReentrantLock 是 Java 里的“显式锁”,基于 AQS 实现,用来解决多线程并发安全问题,功能和synchronized类似(保证同一时间只有一个线程执行临界区代码),但更灵活;

    • 基于 AQS:底层靠 AQS 的“同步队列 + 状态管理”实现线程排队、锁竞争;

    • 互斥锁:同一时间,只能有一个线程拿到锁(保证线程安全);

    • 可重入:同一个线程可以多次获取同一把锁(不会自己锁死自己);

    • ReentrantLocksynchronized 更灵活 ,比如:

      • 支持公平锁/非公平锁 切换(synchronized只能非公平);

      • 支持限时等待锁tryLock(time)避免死等);

      • 支持多个条件变量Condition),精准控制线程唤醒;

  • 使用方式:

    public class ReentrantLockTest {// 1. 创建 ReentrantLock 实例(默认非公平锁,高效)private final ReentrantLock lock = new ReentrantLock();  public void doSomething() {// 2. 加锁:调用 lock(),线程会阻塞直到拿到锁lock.lock();  try {// 3. 临界区:这里的代码,同一时间只有一个线程能执行//    比如操作共享变量、调用非线程安全的方法等//    ... method body ...} finally {// 4. 释放锁:必须在 finally 里解锁!//    保证即使临界区抛异常,锁也能释放,避免其他线程死等lock.unlock();  }}
    }
    

3.2 原理

  • ReentrantLock 基于 AQS 和 CAS 实现。

3.2.1 lock()

在这里插入图片描述

  • ReentrantLock 构造时,默认是非公平锁,也可以传 true 显式用公平锁lock() 执行时会先判断:

    • 如果是公平锁 → 走 FairSync.lock() 分支

      • 公平锁的核心特点:必须排队,不能插队,流程更严格,适合需要“公平性”的场景;
    • 如果是非公平锁 → 走 NonfairSync.lock() 分支

      • 非公平锁的核心特点:不排队,直接尝试“插队”抢锁 ,流程更短,适合大部分场景;
  • 非公平锁流程(默认,更高效。看黄色、紫色流程)

    • 上来就用 CAS 把 AQS 的state从 0 改成 1 → 成功的话,直接拿到锁,标记当前线程为持有者 → 这一步是“插队”的关键:不管队列里有没有等待线程,新线程先试一次抢锁;

    • 如果state != 0(锁被占用),再判断持有锁的线程是不是当前线程(可重入逻辑):

      • 是 → state +1(重入次数+1);

      • 不是 → 进入同步队列排队,即把当前线程包装成 Node ,加入 AQS 的同步队列尾部 → 线程阻塞,等待被唤醒;

  • 公平锁流程(严格排队,略低效。看红色、紫色流程)

    • 即使state == 0(锁空闲),也要先看同步队列里有没有线程在排队 → 有排队的,当前线程必须加入队列,不能直接抢;

    • 如果队列里没人排队,才用 CAS 抢锁 → 成功则标记线程,失败则进入队列排队;

    • 和非公平锁类似,包装成 Node 加入队列尾部 → 线程阻塞,等待被唤醒;

  • 核心差异:插队 vs 排队

    对比项非公平锁(默认)公平锁
    抢锁逻辑直接 CAS 抢锁(允许插队)先检查队列,没人排队才 CAS 抢锁
    优点减少线程切换,性能更高保证线程公平性,避免“线程饿死”
    缺点可能导致线程饿死(一直插队,队列里的线程永远抢不到)频繁切换线程,性能略低

3.2.2 unlock()

在这里插入图片描述

  • 流程解读:

    • 校验:持有锁的线程是否是当前线程?

      • 判断逻辑:ReentrantLock 是独占锁,只有持有锁的线程才能释放锁;
      • 如果不是:直接抛 IllegalMonitorStateException(比如线程 A 没拿到锁,却调用 unlock());
      • 如果是:继续下一步;
    • 处理重入计数:state -1。ReentrantLock 支持可重入,用 AQS 的 state 变量记录 重入次数

      • state = 0:锁空闲;state > 0:锁被占用(值=重入次数);

      • 释放时,先把 `state -1;

    • 判断 state 是否归 0?

      • 如果 state == 0:表示当前线程彻底释放了锁(重入次数清零)→ 执行唤醒等待队列中的线程逻辑,让队列里的线程重新竞争锁;
  • 如果 state > 0:表示当前线程还在重入中(比如连续 lock() 了 3 次,只 unlock() 了 2 次)→ 只减少重入次数,不唤醒其他线程(因为锁还没完全释放);

    • 唤醒等待线程。当锁彻底释放(state == 0),ReentrantLock 会:

      • 从 AQS 的同步队列(CLH 队列)中取出头部节点(等待最久的线程);

      • 唤醒该线程,让它重新尝试抢锁(调用 acquire() 逻辑);

  • 关键细节

    • 可重入的体现:每次 unlock() 只会让 state -1,直到 state == 0 才真正释放锁 → 支持“多次加锁,对应多次释放”;
    • 唤醒的时机:只有当锁彻底释放(state == 0)时,才会唤醒等待队列的线程 → 保证锁的“独占性”,避免多个线程同时持有锁;
    • 异常保护:非持有锁的线程调用 unlock() 会抛异常 → 避免“非法释放锁”导致的线程安全问题。
  • ReentrantLock.unlock()synchronized 的“自动释放”逻辑类似,但更“显式”:

    • synchronized 靠 JVM 自动释放(退出同步块时),无需手动调用;

    • ReentrantLock 必须手动 unlock() ,但流程更透明(能看到 state 处理和唤醒逻辑)。

3.3 源码分析

3.3.1 构造函数

  • ReentrantLock 的构造函数,直接决定了用公平锁还是非公平锁

    public class ReentrantLock {// 核心:用 Sync 的子类(FairSync/NonfairSync)实现不同策略private final Sync sync;  // 1. 默认构造(非公平锁)public ReentrantLock() {sync = new NonfairSync(); // 默认用“非公平锁”,性能更高}// 2. 显式指定公平性public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); }
    }
    
    • Sync 是 ReentrantLock 的内部抽象类,继承自 AQS → 复用 AQS 的队列和状态管理;

    • FairSync(公平锁)和 NonfairSync(非公平锁)是 Sync 的子类 → 分别实现“公平”和“非公平”的加锁逻辑。

3.3.2 lock()

  • ReentrantLock 的 lock() 很简单,直接调用 sync.lock()

    public void lock() {sync.lock(); // 具体逻辑由 FairSync/NonfairSync 决定
    }
    
  • 公平锁 vs 非公平锁 的差异,全在 Sync 子类的 lock() 实现里;

  • 公平锁(FairSync)的 lock() 逻辑

    final void lock() {acquire(1); // 调用 AQS 的 acquire 方法
    }
    
    • 公平锁的核心:严格排队,不允许插队 ,必须尊重“等待队列”的顺序;

    • acquire(1) 是 AQS 的模板方法,内部会调用 tryAcquire(需要子类实现);

    • FairSync 的 tryAcquire先检查“同步队列是否有等待线程” ,再尝试 CAS 抢锁 → 保证公平性;

  • 非公平锁(NonfairSync)的 lock() 逻辑

    final void lock() {// 1. 直接 CAS 抢锁(插队):把 state 从 0 改成 1if (compareAndSetState(0, 1)) {// 抢锁成功,标记当前线程为持有者setExclusiveOwnerThread(Thread.currentThread());} else {// 2. 抢锁失败,调用 AQS 的 acquire 排队acquire(1);}
    }
    
    • 非公平锁的核心:允许插队,直接抢锁 ,性能更高(减少线程切换);

    • 上来就用 CAS 抢锁(不管队列里有没有线程)→ 这是“非公平”的核心(插队);

    • 如果 CAS 失败(锁被占用或有竞争),才会进入 AQS 的队列排队 → 退而求其次。

3.3.3 acquire()

  • acquire(int arg)是 AQS 提供的模板方法,定义了抢占资源(加锁)的通用流程:

    • 先尝试抢锁(调用 tryAcquire,由子类实现,比如 ReentrantLock 的 FairSync/NonfairSync);

    • 抢锁失败 → 进入“同步队列”排队 → 阻塞线程,等待被唤醒;

  • 源码:

    public final void acquire(int arg) {// 1. 尝试抢锁:调用子类重写的 tryAcquireif (!tryAcquire(arg) && // 2. 抢锁失败 → 入队 + 阻塞acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 3. 处理中断(可选,标记当前线程被中断)selfInterrupt();
    }
    
    • tryAcquire(arg):尝试抢锁(子类自定义逻辑)

      • 作用:调用子类(如 ReentrantLock 的 FairSync/NonfairSync)重写的 tryAcquire,尝试抢占资源(锁);

      • 返回 true:抢锁成功,直接返回(不执行后续逻辑);

      • 返回 false:抢锁失败,进入“入队 + 阻塞”流程;

    • addWaiter(Node.EXCLUSIVE):把线程包装成 Node,加入同步队列

      • Node.EXCLUSIVE:标记节点为“独占模式”(适合 ReentrantLock 这类独占锁);

      • addWaiter 逻辑

        • 把当前线程包装成 Node(AQS 里的双向链表节点);
        • 尝试快速把 Node 加入同步队列的尾部(优化:先尝试 CAS 加尾节点,失败则遍历队列找尾节点);
    • acquireQueued(Node node, int arg):线程入队后,阻塞等待

      • 作用:让入队的线程阻塞等待,直到被唤醒并成功抢到锁;

      • 核心逻辑

        • 循环检查前驱节点:如果当前节点是队列头部的下一个节点,再次尝试抢锁(tryAcquire);
        • 抢锁成功 → 出队,返回 false(不执行 selfInterrupt);
        • 抢锁失败 → 判断是否需要阻塞线程(通过前驱节点的状态,比如 SIGNAL);
        • 线程阻塞后,会在“被唤醒、超时、中断”时醒来,再次尝试抢锁;
    • selfInterrupt():标记线程中断

      • 作用:如果线程在 acquireQueued 中被中断唤醒,调用 selfInterrupt() 标记“当前线程被中断”(记录中断状态,不立即处理);

      • 注意:AQS 的 acquire 不会“响应中断”(不会抛出 InterruptedException),而是让子类自己决定如何处理中断;

  • 核心流程总结

当前线程AQSReentrantLock的Sync子类(Fair/Nonfair)调用 acquire(arg)tryAcquire(arg) → 尝试抢锁返回 true直接返回,不阻塞返回 falseaddWaiter → 包装成Node,加入同步队列尾部acquireQueued → 循环抢锁 + 阻塞返回 false → 不执行 selfInterrupt返回 true → 执行 selfInterrupt(标记中断)alt[被唤醒后抢锁成功][被中断唤醒]alt[抢锁成功][抢锁失败]当前线程AQSReentrantLock的Sync子类(Fair/Nonfair)
  • “尝试 + 排队 + 阻塞”的意义

    • tryAcquire 快速尝试:避免每次都入队,减少线程切换开销(非公平锁的 CAS 抢锁就是典型);

    • 同步队列(CLH 队列):用双向链表管理等待线程,保证“FIFO”(先进先出),配合 acquireQueued 的循环抢锁,实现高效的“唤醒-竞争”逻辑;

    • 阻塞与唤醒:通过 LockSupport.park() 阻塞线程,unpark() 唤醒线程,避免无效 CPU 空转;

  • 和 ReentrantLock 的关联:ReentrantLock 的 lock() 最终调用 acquire(1),而 tryAcquire 的逻辑由 FairSyncNonfairSync 实现

    • 公平锁tryAcquire 会检查队列,保证“排队优先”;

    • 非公平锁tryAcquire 会直接 CAS 抢锁(插队),失败再入队。

3.3.4 tryAcquire()

  • 公平锁(FairSync)的 tryAcquire:严格排队,不插队

    • 公平锁的核心是必须尊重等待队列的顺序,即使锁空闲,也要先看有没有线程在队列里排队;

    • 源码:

      // 公平锁 FairSync#tryAcquire() 方法
      protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // AQS的state:0=空闲,>0=占用(重入次数)if (c == 0) { // 锁空闲时// 关键:先检查队列里有没有等待的线程if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current);return true;}} // 重入逻辑:当前线程已持有锁,state+1else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
      }
      
      • hasQueuedPredecessors():判断同步队列中是否有线程在排队;

        • 如果有 → 即使锁空闲,当前线程也不能直接抢(必须排队);
        • 如果没有 → 才能用 CAS 抢锁;
      • 重入逻辑和非公平锁一致:当前线程已持有锁时,state 递增(支持可重入)。

  • 非公平锁(NonfairSync)的 tryAcquire:允许插队,直接抢

    • 非公平锁的核心是 “不排队,直接尝试抢锁”,性能更高(减少线程切换);

    • 源码:

      // 非公平锁 NonFairSync#tryAcquire() 方法
      protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);
      }// 非公平锁 Sync#nonfairTryAcquire() 方法
      final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) { // 锁空闲时// 关键:不检查队列,直接CAS抢锁if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current);return true;}} // 重入逻辑:和公平锁一致else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
      }
      
      • 锁空闲时,不调用 hasQueuedPredecessors() → 不管队列里有没有线程,直接用 CAS 抢锁;

      • 这就是非公平的体现:新线程可以“插队”,不用排队,直接抢锁;

  • ReentrantLock 默认是非公平锁,因为:

    • 性能更高:避免了“检查队列”的开销,且“插队抢锁”能减少线程切换(不用频繁入队、阻塞、唤醒);

    • 大多数场景不需要“绝对公平”:业务上通常更关注性能,而非严格的排队顺序。

3.3.5 addWaiter()

  • addWaiter(Node mode) 的作用是:

    • 当前线程包装成 AQS 的 Node 节点(标记为“共享模式”或“独占模式”);

      • SHARED共享模式(比如 Semaphore 允许多线程同时获取许可);
      • EXCLUSIVE独占模式(比如 ReentrantLock 同一时间只允许一个线程持有锁);
    • 把这个 Node 加入 AQS 的同步队列尾部,等待被唤醒抢锁;

  • 源码:

    private Node addWaiter(Node mode) {// 1. 把当前线程和模式(SHARED/EXCLUSIVE)封装到 Node 中 → 后续队列用 Node 管理线程Node node = new Node(Thread.currentThread(), mode);// 2. 尝试快速入队(先取队列尾节点)Node pred = tail;if (pred != null) { // 队列不为空node.prev = pred; // 当前节点的前驱指向原尾节点// CAS 把尾节点设为当前节点if (compareAndSetTail(pred, node)) {pred.next = node; // 原尾节点的后继指向当前节点return node;}}// 3. 快速入队失败 → 调用 enq 方法,自旋入队enq(node);return node;
    }
    

3.3.6 enq()

  • enq 的作用是:不管队列是否为空,都要把节点 node 加入同步队列的尾部。如果队列为空,还会先初始化队列;

  • 源码:

    private Node enq(final Node node) {// 1. 自旋:直到节点成功入队for (;;) {Node t = tail; // 获取队列的尾节点// 2. 队列为空 → 初始化队列if (t == null) { // CAS 初始化头节点(空节点)if (compareAndSetHead(new Node())) tail = head; // 头、尾节点都指向这个空节点} else { // 3. 队列不为空 → 把节点加入尾部node.prev = t; // 当前节点的前驱指向原尾节点// CAS 把尾节点设为当前节点if (compareAndSetTail(t, node)) {t.next = node; // 原尾节点的后继指向当前节点 → 完成双向链表的连接return t; // 返回原尾节点}}}
    }
    
  • enq 是 AQS 中节点入队的兜底逻辑

    • 解决队列为空时的初始化问题;

    • 自旋 + CAS 保证多线程并发入队时,节点能安全地加入队列尾部,不会出现队列断裂或节点丢失;

  • addWaiter 方法会先尝试快速入队(直接 CAS 设尾节点),如果失败,就会调用 enqenq 用自旋保证一定入队成功。

3.3.7 acquireQueued()

  • acquireQueued 是 AQS 中线程入队后,如何等待锁的核心逻辑,流程是:自旋抢锁 → 抢不到则阻塞 → 被唤醒后再抢

  • 源码:

    final boolean acquireQueued(final Node node, int arg) {boolean failed = true; // 标记“是否成功获取锁”try {boolean interrupted = false;for (;;) { // 自旋:直到抢到锁或被中断final Node p = node.predecessor(); // 获取前驱节点// 若前驱是头节点(说明自己是的第一个有值的节点) → 尝试抢锁(头节点代表“当前持有锁的线程”)if (p == head && tryAcquire(arg)) {setHead(node); // 抢锁成功 → 把当前节点设为新头节点p.next = null; // 原头节点的next置空,方便GC回收failed = false; // 标记“成功获取锁”return interrupted; // 返回是否被中断过}// 抢锁失败 → 判断是否需要阻塞当前线程if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { // 阻塞线程,返回是否被中断interrupted = true;}}} finally {if (failed) // 若获取锁失败(如异常),取消当前节点的抢锁cancelAcquire(node);}
    }
    
    • 自旋抢锁:只要前驱是头节点(说明自己是的第一个有值的节点),就尝试tryAcquire抢锁(由子类实现,比如 ReentrantLock 的公平/非公平逻辑);

    • 抢锁成功:更新头节点,断开原头节点引用,保证 GC 回收;

    • 抢锁失败:调用 shouldParkAfterFailedAcquire 判断是否要阻塞,若需要则用 park 挂起线程,等待被唤醒;

  • shouldParkAfterFailedAcquire(Node pred, Node node)acquireQueued 的“辅助判断”,核心是检查“前驱节点的状态”,决定当前线程是否该阻塞;

    • AQS 的 Node 类用 waitStatus 标记节点状态:

      • CANCELLED (1):节点已取消(比如线程中断、超时),需从队列中移除;

      • SIGNAL (-1):节点的后继节点需要被唤醒(当前节点释放锁时,要唤醒后继);

      • CONDITION (-2):节点在“条件队列”中(和 Condition 相关,非同步队列逻辑);

      • PROPAGATE (-3):共享模式下,需要“传播”唤醒信号(比如 Semaphore 释放许可时);

    • 源码:

      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus; // 获取前驱节点的状态if (ws == Node.SIGNAL) { // 前驱状态是 SIGNAL → 说明当前节点需要被唤醒,所以可以阻塞return true;}if (ws > 0) { // 前驱状态是 CANCELLED → 跳过取消的节点,重新找有效的前驱do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node; // 把当前节点挂到有效前驱下} else { // 前驱状态是 0/CONDITION/PROPAGATE → CAS 把前驱状态设为 SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false; // 暂时不阻塞,下次自旋再判断
      }
      
      • 前驱是 SIGNAL:直接返回 true,当前线程可以阻塞(因为前驱会唤醒它);

      • 前驱是 CANCELLED:清理“取消的节点”,把当前节点挂到有效前驱下,保证队列有效性;

      • 其他状态:通过 CAS 把前驱状态设为 SIGNAL,下次自旋时再判断是否阻塞;

  • 整体流程:线程如何在队列中“等待锁”?

    • 线程入队后,进入 acquireQueued 自旋抢锁;
    • 若前驱是头节点,尝试 tryAcquire 抢锁 → 成功则更新头节点;
    • 抢锁失败,调用 shouldParkAfterFailedAcquire 检查前驱状态 → 决定是否阻塞;
    • 若需要阻塞,用 park 挂起线程,直到被唤醒(比如锁释放时,前驱节点调用 unpark)。

3.3.8 unlock()

  • ReentrantLock 的 unlock() 代码非常简洁,直接调用 sync.release(1)

    public void unlock() {sync.release(1); // 实际逻辑由 AQS 的 release 实现
    }
    
    • sync 是 ReentrantLock 内部的 Sync 子类(FairSync/NonfairSync),但 release 方法最终由 AQS 实现
  • AQS 的 release 是“释放锁”的核心模板方法,流程是:尝试释放锁 → 释放成功则唤醒队列中的等待线程

    public final boolean release(int arg) {// 1. 尝试释放锁:调用子类(如 ReentrantLock)重写的 tryReleaseif (tryRelease(arg)) { Node h = head; // 获取同步队列的头节点// 2. 头节点存在,且状态不是 0 → 唤醒后续等待的节点if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒头节点的后继节点return true; // 释放成功}return false; // 释放失败(比如重入次数没清完)
    }
    
    • tryRelease(arg)

      • 由子类(如 ReentrantLock)重写,实现具体的释放锁逻辑;
      • 对 ReentrantLock 来说,tryRelease减少重入计数(state -1),如果 state == 0,则彻底释放锁(标记当前线程为 null);
    • unparkSuccessor(h):当锁彻底释放后,唤醒同步队列中头节点的后继节点;

  • 整体流程:unlock() 如何唤醒等待线程?

    • ReentrantLock 的 unlock() → 调用 sync.release(1)
    • AQS 的 release(1) → 调用 tryRelease 释放锁(重入计数减 1,直到 state == 0);
    • 锁彻底释放后 → 调用 unparkSuccessor,从同步队列中找到最前面的有效节点,唤醒其对应的线程;
    • 被唤醒的线程 → 回到 acquireQueued 中,再次尝试抢锁(tryAcquire)。

3.3.9 tryRelease()

  • tryRelease 是 AQS 定义的抽象方法,由子类(如 ReentrantLock 的 Sync)重写,实现具体的释放锁逻辑;

    protected final boolean tryRelease(int releases) {// 1. 计算释放后的 state 值(state 是 AQS 的核心变量,代表“重入次数”)int c = getState() - releases; // 2. 校验:只有“持有锁的线程”才能释放锁if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException(); // 3. 标记“是否彻底释放锁”(state == 0 表示彻底释放)boolean free = false;if (c == 0) { // state 减到 0 → 彻底释放锁free = true;setExclusiveOwnerThread(null); // 清除“锁的持有者”标记}// 4. 更新 state 的值setState(c); // 5. 返回是否“彻底释放锁”return free; 
    }
    
    • AQS 的 state 变量,在 ReentrantLock 中代表 重入次数

      • state = 0:锁空闲;
      • state > 0:锁被占用(值 = 重入次数);
    • 释放时的重入计数递减:每次 tryRelease 会让 state = state - releasesreleases 通常为 1,对应一次 unlock());

    • 持有锁线程校验:只有“当前线程 == 锁的持有者线程”,才能释放锁 → 否则抛 IllegalMonitorStateException(防止其他线程非法释放锁);

    • 彻底释放的判断:当 state == 0 时,标记 free = true,并清除锁的持有者(setExclusiveOwnerThread(null))→ 表示锁完全空闲,其他线程可竞争;

    • 返回值的意义

      • 返回 true:锁被彻底释放state == 0)→ AQS 会唤醒同步队列中的等待线程;
      • 返回 false:锁只是减少了重入次数state > 0)→ 还没完全释放,不唤醒其他线程;
  • ReentrantLock 的 unlock() 最终会调用 AQS 的 release 方法,而 release 内部会调用 tryRelease

    • tryRelease 返回 true(彻底释放锁),release 会唤醒同步队列中的等待线程;

    • 若返回 false(只是减少重入次数),则不唤醒线程(锁还没完全释放)。

在这里插入图片描述

http://www.dtcms.com/a/357302.html

相关文章:

  • 编程设计模式
  • 【系列02】端侧AI:构建与部署高效的本地化AI模型 第1章:为什么是端侧AI?
  • 【LINUX】常用基本指令(1)
  • go 使用rabbitMQ
  • 神经网络|(十六)概率论基础知识-伽马函数·中
  • Hugging Face入门指南:AI创客的数字游乐场
  • 解析json
  • LeetCode 142.环形链表 II
  • 【前端教程】JavaScript 数组对象遍历与数据展示实战
  • 动态规划01背包
  • 解锁Libvio访问异常:从故障到修复的全攻略
  • 从“Where”到“Where + What”:语义多目标跟踪(SMOT)全面解读
  • C# 日志写入loki
  • 海外广告流量套利:为什么需要使用移动代理IP?
  • 接吻数问题:从球体堆叠到高维空间的数学奥秘
  • 告别K8s部署繁琐!用KubeOperator可视化一键搭建生产级集群
  • 玄机靶场 | 冰蝎3.0-jsp流量分析
  • ACID分别如何实现
  • Dockerfile实现java容器构建及项目重启(公网和内网)
  • SOME/IP-SD IPv4组播的通信参数由谁指定?
  • React学习教程,从入门到精通, ReactJS - 特性:初学者的指南(4)
  • C++链表双杰:list与forward_list
  • ElasticSearch对比Solr
  • Node.js 的流(Stream)是什么?有哪些类型?
  • DQL单表查询相关函数
  • STM32F2/F4系列单片机解密和芯片应用介绍
  • Ubuntu虚拟机磁盘空间扩展指南
  • AI视频安防,为幼儿园安全保驾护航
  • 基于 GPT-OSS 的成人自考口语评测 API 开发全记录
  • 深度解密SWAT模型:遥感快速建模、DEM/LU/气象数据不确定性、子流域/坡度划分、未来土地利用与气候变化情景模拟及措施效益评估