fix cache

This commit is contained in:
Looly 2025-08-22 12:13:17 +08:00
parent 814b26de01
commit fc72a84d07
4 changed files with 63 additions and 46 deletions

View File

@ -85,11 +85,11 @@ public interface Cache<K, V> extends Iterable<V>, Serializable {
* 每次调用此方法会刷新最后访问时间也就是说会重新计算超时时间
*
* @param key
* @param supplier 如果不存在回调方法用于生产值对象
* @param valueFactory 如果不存在回调方法用于生产值对象
* @return 值对象
*/
default V get(final K key, final SerSupplier<V> supplier) {
return get(key, true, supplier);
default V get(final K key, final SerSupplier<V> valueFactory) {
return get(key, true, valueFactory);
}
/**
@ -99,10 +99,12 @@ public interface Cache<K, V> extends Iterable<V>, Serializable {
*
* @param key
* @param isUpdateLastAccess 是否更新最后访问时间即重新计算超时时间
* @param supplier 如果不存在回调方法用于生产值对象
* @param valueFactory 如果不存在回调方法用于生产值对象
* @return 值对象
*/
V get(K key, boolean isUpdateLastAccess, SerSupplier<V> supplier);
default V get(final K key, final boolean isUpdateLastAccess, final SerSupplier<V> valueFactory) {
return get(key, isUpdateLastAccess, timeout(), valueFactory);
}
/**
* 从缓存中获得对象当对象不在缓存中或已经过期与当前时间差值大于超时时间返回SerSupplier回调产生的对象否则返回值
@ -112,10 +114,10 @@ public interface Cache<K, V> extends Iterable<V>, Serializable {
* @param key
* @param isUpdateLastAccess 是否更新最后访问时间即重新计算超时时间
* @param timeout 自定义超时时间
* @param supplier 如果不存在回调方法用于生产值对象
* @param valueFactory 如果不存在回调方法用于生产值对象
* @return 值对象
*/
V get(K key, boolean isUpdateLastAccess, final long timeout, SerSupplier<V> supplier);
V get(K key, boolean isUpdateLastAccess, final long timeout, SerSupplier<V> valueFactory);
/**
* 从缓存中获得对象当对象不在缓存中或已经过期与当前时间差值大于超时时间返回{@code null}否则返回值

View File

@ -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<K, V> implements Cache<K, V> {
}
@Override
public V get(final K key, final boolean isUpdateLastAccess, final SerSupplier<V> supplier) {
return get(key, isUpdateLastAccess, this.timeout, supplier);
}
/**
* 相同线程key缓存用于检查key循环引用导致的死锁
*/
private final ThreadLocal<Set<K>> loadingKeys = ThreadLocal.withInitial(HashSet::new);
@Override
public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier<V> supplier) {
public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier<V> valueFactory) {
V v = get(key, isUpdateLastAccess);
if (null == v && null != supplier) {
// 在尝试加锁前检查当前线程是否已经在加载这个 keyissue#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<K, V> implements Cache<K, V> {
// 因此此处需要使用带全局锁的get获取值
v = get(key, isUpdateLastAccess);
if (null == v) {
loadingKeys.get().add(key);
// supplier的创建是一个耗时过程此处创建与全局锁无关而与key锁相关这样就保证每个key只创建一个value且互斥
try {
v = supplier.get();
v = valueFactory.get();
put(key, v, timeout);
} finally {
// 无论 supplier 执行成功还是失败都必须在 finally 块中移除标记
loadingKeys.get().remove(key);
}
}
} finally {
keyLock.unlock();
@ -226,7 +204,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
*/
@Override
public long timeout() {
return timeout;
return this.timeout;
}
/**

View File

@ -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<K, V> implements Cache<K, V> {
@Serial
private static final long serialVersionUID = 1L;
@Override
@ -67,18 +69,8 @@ public class NoCache<K, V> implements Cache<K, V> {
}
@Override
public V get(final K key, final SerSupplier<V> supplier) {
return get(key, true, supplier);
}
@Override
public V get(final K key, final boolean isUpdateLastAccess, final SerSupplier<V> supplier) {
return get(key, isUpdateLastAccess, 0, supplier);
}
@Override
public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier<V> supplier) {
return (null == supplier) ? null : supplier.get();
public V get(final K key, final boolean isUpdateLastAccess, final long timeout, final SerSupplier<V> valueFactory) {
return (null == valueFactory) ? null : valueFactory.get();
}
@Override

View File

@ -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<String, String> cache = new LRUCache<>(100);
final String key1 = "key1";
final String key2 = "key2";
final CountDownLatch latch = new CountDownLatch(2);
// 线程1key1 -> key2
ThreadUtil.execute(() -> {
try {
final String result = cache.get(key1, () -> {
Thread.sleep(100);
return cache.get(key2, () -> "value2") + "-nested";
});
}
finally {
latch.countDown();
}
});
// 线程2key2 -> 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("检测到可能的死锁!");
}
}
}