From 03a7312e23097677664dfc3e89b51b335b0e8d12 Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 21 Aug 2025 11:37:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D`AbstrachCache.get`=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E9=80=A0=E6=88=90=E7=9A=84=E6=AD=BB=E9=94=81=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=88issue#4022@Github=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../cn/hutool/cache/impl/AbstractCache.java | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315c658b5..3b4a589e8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * 【extra 】 修复`QLExpressEngine`allowClassSet无效问题(issue#3994@Github) * 【core 】 修复`StrBuilder`insert插入计算错误问题(issue#ICTSRZ@Gitee) * 【cron 】 修复`CronPatternUtil.nextDateAfter`计算下一个匹配表达式的日期时,计算错误问题(issue#4006@Github) +* 【cach 】 修复`AbstrachCache.get`可能造成的死锁问题(issue#4022@Github) ------------------------------------------------------------------------------------------------------------- # 5.8.39(2025-06-20) diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java index 5ff371789..337b682c6 100755 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -7,6 +7,7 @@ import cn.hutool.core.lang.mutable.Mutable; import cn.hutool.core.lang.mutable.MutableObj; import cn.hutool.core.map.SafeConcurrentHashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; @@ -65,6 +66,11 @@ public abstract class AbstractCache implements Cache { */ protected CacheListener listener; + /** + * 相同线程key缓存,用于检查key循环引用导致的死锁 + */ + private final ThreadLocal> loadingKeys = ThreadLocal.withInitial(HashSet::new); + // ---------------------------------------------------------------- put start @Override public void put(K key, V object) { @@ -126,6 +132,12 @@ public abstract class AbstractCache implements Cache { public V get(K key, boolean isUpdateLastAccess, long timeout, Func0 supplier) { V v = get(key, isUpdateLastAccess); if (null == v && null != supplier) { + // 在尝试加锁前,检查当前线程是否已经在加载这个 key,见:issue#4022 + // 如果是,则说明发生了循环依赖。 + if (loadingKeys.get().contains(key)) { + throw new IllegalStateException("Circular dependency detected for key: " + key); + } + //每个key单独获取一把锁,降低锁的粒度提高并发能力,see pr#1385@Github final Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock()); keyLock.lock(); @@ -135,9 +147,15 @@ public abstract class AbstractCache implements Cache { // 因此此处需要使用带全局锁的get获取值 v = get(key, isUpdateLastAccess); if (null == v) { + loadingKeys.get().add(key); // supplier的创建是一个耗时过程,此处创建与全局锁无关,而与key锁相关,这样就保证每个key只创建一个value,且互斥 - v = supplier.callWithRuntimeException(); - put(key, v, timeout); + try { + v = supplier.callWithRuntimeException(); + put(key, v, timeout); + } finally { + // 无论 supplier 执行成功还是失败,都必须在 finally 块中移除标记 + loadingKeys.get().remove(key); + } } } finally { keyLock.unlock();