diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/cache/Cache.java b/hutool-core/src/main/java/cn/hutool/v7/core/cache/Cache.java index 3c8eb1eac..1996183bc 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/cache/Cache.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/cache/Cache.java @@ -85,11 +85,11 @@ public interface Cache extends Iterable, Serializable { * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。 * * @param key 键 - * @param supplier 如果不存在回调方法,用于生产值对象 + * @param valueFactory 如果不存在回调方法,用于生产值对象 * @return 值对象 */ - default V get(final K key, final SerSupplier supplier) { - return get(key, true, supplier); + default V get(final K key, final SerSupplier valueFactory) { + return get(key, true, valueFactory); } /** @@ -99,10 +99,12 @@ public interface Cache extends Iterable, Serializable { * * @param key 键 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。 - * @param supplier 如果不存在回调方法,用于生产值对象 + * @param valueFactory 如果不存在回调方法,用于生产值对象 * @return 值对象 */ - V get(K key, boolean isUpdateLastAccess, SerSupplier supplier); + default V get(final K key, final boolean isUpdateLastAccess, final SerSupplier valueFactory) { + return get(key, isUpdateLastAccess, timeout(), valueFactory); + } /** * 从缓存中获得对象,当对象不在缓存中或已经过期(与当前时间差值大于超时时间)返回SerSupplier回调产生的对象,否则返回值。 @@ -112,10 +114,10 @@ public interface Cache extends Iterable, Serializable { * @param key 键 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。 * @param timeout 自定义超时时间 - * @param supplier 如果不存在回调方法,用于生产值对象 + * @param valueFactory 如果不存在回调方法,用于生产值对象 * @return 值对象 */ - V get(K key, boolean isUpdateLastAccess, final long timeout, SerSupplier supplier); + V get(K key, boolean isUpdateLastAccess, final long timeout, SerSupplier valueFactory); /** * 从缓存中获得对象,当对象不在缓存中或已经过期(与当前时间差值大于超时时间)返回{@code null},否则返回值。 diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/AbstractCache.java b/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/AbstractCache.java index 4179ce4aa..a9d1b6002 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/AbstractCache.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/AbstractCache.java @@ -23,7 +23,6 @@ import cn.hutool.v7.core.lang.mutable.Mutable; import cn.hutool.v7.core.lang.mutable.MutableObj; import java.io.Serial; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; @@ -141,24 +140,9 @@ public abstract class AbstractCache implements Cache { } @Override - public V get(final K key, final boolean isUpdateLastAccess, final SerSupplier supplier) { - return get(key, isUpdateLastAccess, this.timeout, supplier); - } - - /** - * 相同线程key缓存,用于检查key循环引用导致的死锁 - */ - private final ThreadLocal> loadingKeys = ThreadLocal.withInitial(HashSet::new); - @Override - public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier supplier) { + public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier valueFactory) { 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); - } - + if (null == v && null != valueFactory) { //每个key单独获取一把锁,降低锁的粒度提高并发能力,see pr#1385@Github final Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock()); keyLock.lock(); @@ -168,15 +152,9 @@ public abstract class AbstractCache implements Cache { // 因此此处需要使用带全局锁的get获取值 v = get(key, isUpdateLastAccess); if (null == v) { - loadingKeys.get().add(key); // supplier的创建是一个耗时过程,此处创建与全局锁无关,而与key锁相关,这样就保证每个key只创建一个value,且互斥 - try { - v = supplier.get(); - put(key, v, timeout); - } finally { - // 无论 supplier 执行成功还是失败,都必须在 finally 块中移除标记 - loadingKeys.get().remove(key); - } + v = valueFactory.get(); + put(key, v, timeout); } } finally { keyLock.unlock(); @@ -226,7 +204,7 @@ public abstract class AbstractCache implements Cache { */ @Override public long timeout() { - return timeout; + return this.timeout; } /** diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/NoCache.java b/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/NoCache.java index ad239815c..6841f1093 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/NoCache.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/cache/impl/NoCache.java @@ -19,6 +19,7 @@ package cn.hutool.v7.core.cache.impl; import cn.hutool.v7.core.cache.Cache; import cn.hutool.v7.core.func.SerSupplier; +import java.io.Serial; import java.util.Iterator; /** @@ -29,6 +30,7 @@ import java.util.Iterator; * @author Looly, jodd, VampireAchao */ public class NoCache implements Cache { + @Serial private static final long serialVersionUID = 1L; @Override @@ -67,18 +69,8 @@ public class NoCache implements Cache { } @Override - public V get(final K key, final SerSupplier supplier) { - return get(key, true, supplier); - } - - @Override - public V get(final K key, final boolean isUpdateLastAccess, final SerSupplier supplier) { - return get(key, isUpdateLastAccess, 0, supplier); - } - - @Override - public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier supplier) { - return (null == supplier) ? null : supplier.get(); + public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier valueFactory) { + return (null == valueFactory) ? null : valueFactory.get(); } @Override diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/cache/CacheConcurrentTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/cache/CacheConcurrentTest.java index 5ebb1c11e..9b916a50e 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/cache/CacheConcurrentTest.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/cache/CacheConcurrentTest.java @@ -19,6 +19,8 @@ package cn.hutool.v7.core.cache; import cn.hutool.v7.core.cache.impl.FIFOCache; import cn.hutool.v7.core.cache.impl.LRUCache; import cn.hutool.v7.core.cache.impl.WeakCache; +import cn.hutool.v7.core.exception.HutoolException; +import cn.hutool.v7.core.func.SerSupplier; import cn.hutool.v7.core.lang.Console; import cn.hutool.v7.core.thread.ConcurrencyTester; import cn.hutool.v7.core.thread.ThreadUtil; @@ -26,6 +28,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** @@ -121,4 +127,43 @@ public class CacheConcurrentTest { // 总耗时应与单次操作耗时在同一个数量级 Assertions.assertTrue(interval < delay * 2); } + + @Test + void issue4022Test() throws InterruptedException { + final Cache cache = new LRUCache<>(100); + + final String key1 = "key1"; + final String key2 = "key2"; + + final CountDownLatch latch = new CountDownLatch(2); + // 线程1:key1 -> key2 + ThreadUtil.execute(() -> { + try { + final String result = cache.get(key1, () -> { + Thread.sleep(100); + return cache.get(key2, () -> "value2") + "-nested"; + }); + } + finally { + latch.countDown(); + } + }); + + // 线程2:key2 -> key1 + ThreadUtil.execute(() -> { + try { + final String result = cache.get(key2, () -> { + Thread.sleep(100); + return cache.get(key1, () -> "value1") + "-nested"; + }); + } finally { + latch.countDown(); + } + }); + + // 设置超时检测死锁 + if (!latch.await(5, TimeUnit.SECONDS)) { + throw new HutoolException("检测到可能的死锁!"); + } + } }