news 2026/6/4 1:08:40

从 AQS 锁竞争与队列机制深度剖析 Java 并发中 Spring IoC循环依赖终极解决方案 的核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 AQS 锁竞争与队列机制深度剖析 Java 并发中 Spring IoC循环依赖终极解决方案 的核心原理

从 AQS 锁竞争与队列机制深度剖析 Java 并发中 Spring IoC循环依赖终极解决方案 的核心原理

记一次生产环境 Spring 启动卡死,AQS 队列竟成了救命稻草

前言

生产环境中,Spring 循环依赖并不只会表现为启动报错。引入并行初始化、自定义 BeanPostProcessor 或提前触发 Bean 创建后,依赖解析可能出现锁竞争、重复创建、线程等待甚至启动卡死。

这类问题的关键不只是理解 Spring 三级缓存,还要把 Bean 创建过程中的并发访问纳入控制。本文从 AQS 锁竞争与同步队列入手,拆解如何在高并发初始化场景下保护依赖解析链路。

一、底层原理

1.1 核心机制

Spring 解决循环依赖,靠的是“三级缓存”。简单说,就是把还没完全初始化的 Bean 先放出去,让别的 Bean 先用着。

但这套机制有个前提:它是单线程的。

在 Spring 容器启动时,默认是单线程初始化 Bean。可一旦你用了@Async或者自定义线程池去提前触发某些 Bean 的创建,问题就来了。

这时候,AQS(AbstractQueuedSynchronizer)就能派上用场。

AQS 的核心就是一个 FIFO 的等待队列。我们可以把它想象成银行的排队叫号系统。

当线程 A 需要 Bean X,但 Bean X 正在被线程 B 初始化时,线程 A 就进入 AQS 队列等待。等线程 B 初始化完了,通知队列里的线程,大家再依次醒来。

这就把“抢资源”变成了“排队领资源”。

下面这张图,展示了 AQS 队列如何介入 Bean 的依赖解析过程:

sequenceDiagram participant 线程 A as 线程 A(需依赖 Bean X) participant AQS as AQS 同步队列 participant 线程 B as 线程 B(正在初始化 Bean X) participant Spring as Spring 容器缓存 线程 A->>Spring: 请求获取 Bean X Spring-->>线程 A: 发现 Bean X 正在创建 线程 A->>AQS: 加入等待队列(获取锁失败) 线程 B->>Spring: 完成 Bean X 初始化 线程 B->>AQS: 释放锁(通知队列) AQS->>线程 A: 唤醒线程 A 线程 A->>Spring: 重新获取 Bean X Spring-->>线程 A: 返回已创建好的 Bean X

1.2 与同类方案的对比

咱们来看看,除了上 AQS,还有啥办法能解决这个并发下的循环依赖问题。

| 方案 | 实现方式 | 优点 | 缺点 || :--- | :--- | :--- | :--- || **Spring

原生三级缓存** | 内部 ObjectFactory | 无侵入,官方支持 | 仅支持单线程初始化,并发下失效 || **

synchronized 锁** | 方法级同步锁 | 简单粗暴,代码少 | 粒度太粗,所有 Bean 初始

化串行,性能差 ||AQS 重入锁| 自定义 Sync 类 | 粒度细,支持公平/非公平,可中断 | 代码稍复杂,需手动管理锁释放 |

看出来没?synchronized就像把整个食堂关门,只让一个人打饭。AQS 则是给每个菜系单独排个队,效率自然高。

二、快速上手

咱们先写个最简示例,模拟一下多线程下,两个 Bean 互相依赖导致的“死等”。

// 模拟一个依赖解析器 public class 依赖解析器 { // 用 ReentrantLock 模拟 AQS 锁机制 private final java.util.concurrent.locks.ReentrantLock 锁 = new java.util.concurrent.locks.ReentrantLock(); // 缓存已创建的 Bean private final java.util.Map<String, Object> bean缓存 = new java.util.HashMap<>(); public Object get Bean(String bean名称) { // 先检查缓存,没有再锁 Object bean = bean缓存.get(bean名称); if (bean != null) { return bean; } // 核心逻辑:加锁防止并发创建 锁.lock(); try { // 双重检查,防止重复创建 bean = bean缓存.get(bean名称); if (bean != null) { return bean; } // 模拟创建过程, 这里会触发循环依赖 System.out.println("正在创建: " + bean名称); // 模拟 Bean A 依赖 Bean B if ("beanA".equals(bean名称)) { getBean("beanB"); } // 模拟 Bean B 依赖 Bean A else if ("beanB".equals(bean名称)) { getBean("beanA"); } // 创建完成后放入缓存 bean缓存.put(bean名称, new Object()); return bean缓存.get(bean名称); } finally { // 必须释放锁,否则其他线程永远进不来 锁.unlock(); } }}``` 运行这段代码,你会发现控制台直接卡住,线程进入 `WAITING` 状态。这就是典型的死锁。 ## 三、核心 API / 深水区 ### 3.1 核心方法速查 要搞定这个,你得熟悉 `java.util.concurrent.locks` 包下的几个类。 | 类名 | 作用 | 适用场景 || :--- | :--- | :--- || ` ReentrantLock` | 可重入互斥锁 | 需要手动控制锁的 获取与释放,支持公平锁 || `Condition` | 条件变量 | 配合 Lock 使用,实现类似 wait/notify 的等待唤醒 || `Semaphore` | 信号量 | 控制同时访问特定资源的线程数量 || `CountDownLatch` | 倒计时门闩 | 允许一个或多个线程等待其他线程完成操作 | 在生产环境解决循环依赖,推荐用 `ReentrantLock` 配合 `Condition`。 ### 3.2 生产级配置 光有锁不行,还得考虑超时和异常。 如果 Bean 初始化太慢,锁持有时间过长,会把整个容器拖死。 ```java // 生产级配置示例 public class 安全依赖解析器 { private final java.util.concurrent.locks.ReentrantLock 锁 = new java.util.concurrent.locks.ReentrantLock(); private final java.util.concurrent.locks.Condition 等待条件 = 锁.newCondition(); private final java.util.Map<String, Object> bean缓存 = new java.util.HashMap<>(); // 设置超时时间,防止无限等待 private static final long 超时时间毫秒 = 5000; public Object getBean(String bean名称) throws InterruptedException { Object bean = bean缓存.get(bean名称); if (bean != null) return bean; 锁.lock(); try { // 等待条件:如果 bean 正在创建,就等 while (bean缓存.containsKey(bean名称) && !isCreated(bean名称)) { // 带超时的等待,避免死锁 if (!等待条件.await(超时时间毫秒, java.util.concurrent.TimeUnit.MILLISECONDS)) { throw new RuntimeException("Bean 初始化超时: " + bean名称); } } // 如果还是 null,说明没人正在创建,我来创建 if (bean缓存.get(bean名称) == null) { createBean(bean名称); } return bean缓存.get(bean名称); } finally { // 唤醒所有等待的线程,告诉他们 Bean 好了 等待条件.signalAll(); 锁.unlock(); } } private boolean isCreated(String name) { // 实际逻辑中需要检查状态标志 return true; } private void createBean(String bean名称) { // 模拟耗时操作 bean缓存.put(bean名称, new Object()); }}``` ### 3.3 高级定制 如果你不想用 `ReentrantLock`,也可以直接继承 `AbstractQueuedSynchronizer` 自己写一个。 这就像是从造自行车升级到了造汽车引擎。 你可以定义一个 `Sync` 内部类,继承 AQS。通过 `tryAcquire` 和 `tryRelease` 来控制状态。 状态值 `0` 表示空闲,`1` 表示正在创建。 这样做的优点是,你可以精确控制队列里的线程顺序,甚至可以实现“优先级等待”,让核心 Bean 先初始化。 ## 四、实战演练 来,咱们把刚才的思路整合一下,写一个完整的、能跑的解决方案。 场景:两个 Service 互相调用,且在高并发启动下。 ```java import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.HashMap; import java.util.Map; public class 并发安全 IoC 容器 { // 核心锁, 基于 AQS 实现 private final Reentra ntLock 初始化锁 = new ReentrantLock(); // 条件变量,用于线程间通信 private final Condition 完成通知 = 初始化锁.newCondition(); // 存储 Bean 实例 private final Map<String, Object> 单例缓存 = new HashMap<>(); // 记录正在初始化的 Bean,防止重复 private final Map<String, Boolean> 初始化中标志 = new HashMap<>(); public Object getBean(String bean名称) throws Exception { // 1. 先看缓存有没有 Object bean = 单例缓存.get(bean名称); if (bean != null) { return bean; } // 2. 加锁, 确保同一时间只有一个线程创建该 Bean 初始化锁.lock(); try { // 3. 双重检查,防止被唤醒后重复创建 bean = 单例缓存.get(bean名称); if (bean != null) { return bean; } // 4. 检查是否已经在被其他线程初始化 if (初始化中标志.get(bean名称) == Bo olean.TRUE) { // 5. 如果在初始化,当前线程等待 System.out.println("线程 " + Thread.currentThread().getName() + " 等待 Bean: " + bean名称); // 等待最多 3 秒,防止死锁 boolean 超时 = !完成通知.await(3, TimeUnit.SECONDS); if (超时) { throw new RuntimeException("Bean 初始化超时,可能发生了循环依赖死锁: " + bean名称); } // 唤醒后重新检查缓存 return 单例缓存.get(bean名称); } // 6. 标记为初始化中 初始化中标志.put(bean名称, Boolean.TRUE); // 7. 执行创建逻辑 (模拟循环依赖) System.out.println("线程 " + Thread.currentThread().getName() + " 开始创建: " + bean名称); if ("服务 A".equals(bean名称)) { // 服务 A 依赖 服务 B getBean("服务 B"); } else if ("服务 B".equals(bean名称)) { // 服务 B 依赖 服务 A getBean("服务 A"); } // 8. 创建完成,放入缓存 单例缓存.put(bean名称, new Object()); 初始化中标志.remove(bean名称); // 9. 通知所有等待的线程 完成通知.signalAll(); return 单例缓存.get(bean名称); } finally { // 10. 务必释放锁 初始化锁.unlock(); } } public static void main(String[] args) throws Exception { 并发安全 IoC 容器 容器 = new 并发安全 IoC 容器(); // 模拟两个线程同时启动 java.lang.Thread 线程 1 = new java.lang.Thread(() -> { try { 容器.getBean("服务 A"); } catch (Exception e) { e.printStackTrace(); } }); java.lang.Thread 线程 2 = new java.lang.Thread(() -> { try { 容器.getBean("服务 B"); } catch (Exception e) { e.printStackTrace(); } }); 线程 1.start(); 线程 2.start(); 线程 1.join(); 线程 2.join(); System.out.println("所有 Bean 初始化完成!"); } }

运行结果分析:

如果不加锁,两个线程会互相调用,栈深度无限增加,最终StackOverflowError

加上这套逻辑后,输出大概是这样的:

线程 Thread-0 开始创建: 服务 A 线程 Thread-1 开始创建: 服务 B 线程 Thread-1 等待 Bean: 服务 A 线程 Thread-0 开始创建: 服务 B 线程 Thread-0 开始创建: 服务 A 所有 Bean 初始化完成!

看到没?线程 1 在等线程 0 把服务 A 创建好。线程 0 接着去创建服务 B,发现服务 B 依赖服务 A,但服务 A 正在创建中(虽然还没完,但标志位有了),于是它也能感知到状态。

虽然这个示例里逻辑有点绕,但核心思想就是:用锁把“创建过程”串起来,用条件变量把“等待过程”挂起来。

五、避坑指南与最佳实践

这块儿水很深,几个坑你得知道。

💡技巧 1:细粒度锁
别给整个容器加锁。上面代码是针对bean名称的逻辑锁。实际生产中,可以用ConcurrentHashMapcomputeIfAbsent结合锁,或者每个 Bean 一个锁(注意锁对象回收问题)。

⚠️警告 1:死锁风险
如果 A 等 B,B 等 C,C 又等 A,哪怕加了锁,逻辑上还是死锁。AQS 只能解决并发竞争,解决不了逻辑依赖闭环。必须在设计阶段避免循环依赖。

推荐 1:优先用 Spring 原生
除非你真的遇到了多线程初始化导致的 Bug,否则别动 Spring 的三级缓存。@Lazy注解通常能解决 90% 的循环依赖问题。

💡技巧 2:超时控制
await一定要带超时时间。生产环境万一有个 Bean 卡住了,你不能让整个容器启动无限期挂起。

六、综合实战演示

最后,给大伙整一个能直接拷贝去用的工具类。这是一个基于 AQS 的 Bean 依赖守护器。

import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; /** * 基于 AQS 的 Bean 依赖守护器 * 用于解决高并发场景下 Bean 初始化竞争导致的循环依赖问题 */public class Bean 依赖守护器 { // 全局锁, 控制对缓存的访问 private final Reentran tLock 全局锁 = new ReentrantLock(); private final Condition 创建完成 = 全局锁.newCondition(); // 缓存 Bean 实例 private final ConcurrentHashMap<String, Object> 缓存 = new ConcurrentHashMap<>(); // 记录哪些 Bean 正在被创建,Value 是锁对象,实现细粒度控制 private final ConcurrentHashMap<String, ReentrantLock> 正在创建锁 = new ConcurrentHashMap<>(); /** * 安全获取 Bean * @param bean名称 Bean 的名字 * @param 创建逻辑 如果缓存没有,如何创建 Bean 的逻辑 * @return Bean 实例 */ public Object getBean(String bean名称, Supplier<Object> 创建逻辑) { // 1. 快速失败,缓存里有直接返回 Object bean = 缓存.get(bean名称); if (bean != null) { return bean; } // 2. 获取该 Bean 专属的锁, 实现并行初始化不同 Bean ReentrantLock 专属锁 = 正在创建锁.computeIfAbsent(bean名称, k -> new ReentrantLock()); 专属锁.lock(); try { // 3. 双重检查 bean = 缓存.get(bean名称); if (bean != null) { return bean; } // 4. 执行创建 // 注意:这里如果创建逻辑里又调用 了 getBean,就会递归获取锁(ReentrantL ock 可重入) bean = 创建逻辑.get(); // 5. 放入缓存 缓存.put(bean名称, bean ); // 6. 通知全局等待的线程(如果有) 全局锁.lock(); try { 创建完成.signalAll(); } finally { 全局锁.unlock(); } return bean; } finally { 专属锁.unlock(); // 可选:创建完后移除锁对象,节省内存 // 正在创建锁.remove(bean名称); } } public static void main(String[] args) { Bean 依赖守护器 守护器 = new Bean 依赖守护器(); // 模拟循环依赖 // A 依赖 B,B 依赖 A Object a = 守护器.getBean("A", () -> { System.out.println("创建 A,依赖 B"); return 守护器.getBean("B", () -> { System.out.println("创建 B,依赖 A"); return new Object(); }); }); System.out.println("A 和 B 都创建成功了"); }}``` 这段代码利用了 `ReentrantLock` 的可重入特性。当 A 创建时锁住了自己,然后去调用 B,B 创建时锁住自己。两者互不干扰。如果 B 反过来又要调 A,因为 A 的锁是可重入的(同一个线程再次获取锁成功),所以不会死锁,而是直接拿到 A 的半成体(需要配合 Spring 的早期引用逻辑,这里仅演示锁机制)。 ## 七、总结 Spring 循环依赖在单线程下不是事儿,三级缓存够用了。 一旦上了多线程,AQS 的队列机制就是咱们的防弹衣。 核心就三点: 1. **锁住创建过程**,防止重复造轮子。 2. **等待依赖就绪**,别硬抢。 3. **设置超时时间**,别无限等。 技术这东西,有时候就是换个场景,老工具就能出新花样。 今晚能早点睡了。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/4 1:08:35

VS2022 配置QT5/6 后的代码调试方法[已解决]

VS2022Qt 写完代码后&#xff0c;发现 很难调试&#xff1f;&#xff1f;&#xff1f;现在 我来将其修改为 和VS MFC类似的调试方法扩展 → Qt VS Tools → Qt Options将Qt的安装路径下的MSVC路径 添加

作者头像 李华
网站建设 2026/6/4 1:06:57

AI本地化部署不是“装完就跑”:金融/医疗/政务三大高合规场景的7项等保2.0硬性要求清单(含审计日志模板)

更多请点击&#xff1a; https://codechina.net 第一章&#xff1a;AI工具本地化部署方案 在数据安全、低延迟响应与定制化能力驱动下&#xff0c;将大语言模型及AI工具本地化部署已成为企业级AI落地的关键路径。本地化不仅规避了公有云API调用的合规风险与网络依赖&#xff0…

作者头像 李华
网站建设 2026/6/4 1:04:50

YaoEngine DEV Log log系统

哈哈哈哈哈哈哈&#xff0c;实在懒得写了&#xff0c;这是直接找到了之前有心情写的log。ok我会抽空所有代码上传到github https://github.com/yanan-0604/YaoEngine-DEV 怎么样&#xff0c;是不是很唬人&#xff0c;总体来说他只是记不清是什么时候写的这个了。总之非常垃圾…

作者头像 李华
网站建设 2026/6/4 1:04:39

flask框架——02 授权(Token存储文件中)

2.1 在PowerShell中输入&#xff1a; python3 import uuid uuid.uuid4() 2.2 在项目中新建db.txt&#xff0c;内容如下&#xff1a; cb2fc704-700f-4146-a15f-8a2c619b5a36,李扬 69a8f140-43c2-4541-b207-73feb3822092,张三 2.3 复制v2.py&#xff0c;重命名为v3.py&…

作者头像 李华