找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1499

积分

0

好友

190

主题
发表于 10 小时前 | 查看: 2| 回复: 0

Spring Framework 近日发布了 v7.0.2 版本,本次更新修复了一个由 ConcurrentReferenceHashMap#computeIfAbsent 方法引发的致命死锁问题(Issue #35944)。该 Bug 可能导致应用在启动时完全卡死,且不产生任何错误日志,因其偶发性而极难排查和复现。

Spring 7 中的死锁陷阱

在 Spring Framework 7.0.2 版本中,包含了一项针对核心并发组件中死锁 Bug 的关键修复。问题的典型现象是:Spring Boot 应用在启动时突然“卡住”,没有任何错误日志,线程 DUMP 显示存在死锁。

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args); // ← 应用在此处卡死!
    }
}

根据社区反馈,此 Bug 并非每次必现,而是在特定的初始化时序和哈希分布下偶然发生,增加了排查难度。

影响范围

  • 影响版本:Spring Framework 6.2.13 - 7.0.1
  • 严重级别:严重(Critical)
  • 影响组件:所有使用 ConcurrentReferenceHashMap 的 Spring 应用
  • 触发条件:复杂的 Spring 上下文初始化时,特定的 Bean 加载顺序和并发访问模式

根因分析

问题的根源在于 Spring 框架内部广泛使用的 ConcurrentReferenceHashMap,尤其是在其 computeIfAbsent 方法的锁实现上。

ConcurrentReferenceHashMap 的作用

ConcurrentReferenceHashMap 是 Spring 框架提供的核心并发数据结构,结合了 ConcurrentHashMap 的并发分段锁机制和弱引用/软引用支持。它在 Spring 内部被用于多种缓存场景,例如注解元数据缓存、类型转换器缓存和 Bean 定义缓存等,是 Spring 框架 高效运行的基础组件之一。

死锁产生的根本原因

根据 Issue #35944 的分析,问题源于一次为修复其他 Bug 而进行的代码提交(commit 12dd758)。该修改导致 mapping 函数在持有分段锁的情况下被执行,形成了锁嵌套的风险。

简化后的问题代码逻辑如下:

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    Segment segment = getSegment(hash); // 获取段锁
    segment.lock(); // 加锁
    try {
        // 危险操作:在锁内执行用户提供的函数!
        V value = mappingFunction.apply(key);
        return put(key, value);
    } finally {
        segment.unlock(); // 解锁
    }
}

死锁场景分析
当用户提供的 mappingFunction 内部再次尝试访问同一个 Map 时,就可能发生循环等待,导致死锁。例如,在 并发编程 中典型的 AB-BA 死锁场景:

  1. 线程1持有 Segment A 锁(对应 key1),尝试获取 Segment B 锁(对应 key2)。
  2. 线程2持有 Segment B 锁(对应 key2),尝试获取 Segment A 锁(对应 key1)。
  3. 两个线程互相等待,形成死锁。

Spring 框架中的实际触发点
在 Spring 上下文初始化期间,注解元数据的加载过程会触发此问题。例如,在 AnnotationTypeMappings.Cache.get() 方法中:

public AnnotationTypeMappings get(Class<? extends Annotation> annotationType) {
    return this.mappings.computeIfAbsent(annotationType, type -> {
        // 在创建子注解映射时,可能递归访问同一个 map
        return createMappings(type); // ← 潜在的再次触发 computeIfAbsent 调用
    });
}

当应用存在多级嵌套注解,且初始化过程涉及并发时,就容易命中这个死锁陷阱。

修复方案

Spring 团队在 v7.0.2 和 v6.2.15 版本中采用了“双重检查加锁 + 解锁重试”的方案来修复此问题。

修复后的核心策略是:将用户提供的映射函数执行移出锁的范围。主要步骤如下:

  1. 无锁快速路径:首先不加锁检查 key 是否存在。
  2. 加锁二次检查:获取锁后再次检查,防止其他线程已插入值。
  3. 关键修复点:在调用 mappingFunction.apply(key) 之前释放锁,确保用户代码在无锁环境下执行。
  4. 重新加锁并插入:计算完新值后,重新获取锁,检查状态,然后执行插入操作。

这种设计避免了在持有锁时执行不可控的用户逻辑,从根本上切断了死锁链,同时通过双重检查保证了线程安全性和数据一致性。

修复要点总结

  1. 隔离用户逻辑:确保自定义函数不在锁区内运行。
  2. 保证线程安全:通过经典的双重检查锁模式处理竞态条件。
  3. 内存可见性:通过合理的锁顺序和 volatile 变量保证 happens-before 关系。

升级建议与验证

如果你正在使用 Spring Framework 6.2.13 至 7.0.1 版本,建议立即升级到修复版本。

Maven 依赖版本更新

<!-- 对于 Spring Boot 3.x / Spring Framework 7.x -->
<properties>
    <spring-framework.version>7.0.2</spring-framework.version>
</properties>

<!-- 对于 Spring Boot 2.7.x / Spring Framework 6.2.x -->
<properties>
    <spring-framework.version>6.2.15</spring-framework.version>
</properties>

升级后验证方法

  1. 线程 DUMP 分析:应用启动后,检查线程 dump 是否还存在 ConcurrentReferenceHashMap 相关的死锁。
    jstack <pid> > threaddump.txt
    grep -i "deadlock" threaddump.txt
  2. 启动监控:观察应用启动时间是否稳定,不再出现随机挂起。
  3. 日志检查(可选):可启用 Spring 核心注解处理的调试日志,观察加载过程是否顺畅。
    logging.level.org.springframework.core.annotation=DEBUG

最佳实践与预防

即使已升级到修复版本,遵循以下 软件开发 最佳实践也有助于避免类似的并发陷阱:

  1. 避免递归调用:尽量避免在 computeIfAbsent 的映射函数中再次调用同一个 Map 的方法。
  2. 保持函数纯净:映射函数应只做纯粹的计算,避免访问共享资源或调用外部服务。
  3. 简化对象构建:避免在映射函数中进行复杂的、可能触发其他动态加载的对象图构建。

总结

Spring Framework 7.0.2 对 ConcurrentReferenceHashMap 死锁 Bug 的修复,体现了一个成熟框架对稳定性的高度重视。该 Bug 的隐蔽性在于其对并发时序的高度依赖,这也提醒开发者,在复杂框架中,即使是基础工具类的细微改动也可能引发系统性风险。及时升级依赖版本,并建立有效的应用启动监控,是保障线上服务稳定的重要手段。




上一篇:德国软件产业现状分析:技术恐惧症、人才外流与生态崩溃
下一篇:elementary OS 8.1升级指南:Wayland安全会话与ARM64支持详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 17:25 , Processed in 0.167313 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表