diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/AbstractCache.java b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/AbstractCache.java index b00ce796a..e7ee0e6ac 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/AbstractCache.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/AbstractCache.java @@ -262,16 +262,10 @@ public abstract class AbstractCache implements Cache { * 移除key对应的对象,不加锁 * * @param key 键 - * @param withMissCount 是否计数丢失数 * @return 移除的对象,无返回null */ - protected CacheObj removeWithoutLock(final K key, final boolean withMissCount) { - final CacheObj co = cacheMap.remove(MutableObj.of(key)); - if (withMissCount) { - // 在丢失计数有效的情况下,移除一般为get时的超时操作,此处应该丢失数+1 - this.missCount.increment(); - } - return co; + protected CacheObj removeWithoutLock(final K key) { + return cacheMap.remove(MutableObj.of(key)); } /** diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/FIFOCache.java b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/FIFOCache.java index e5b508c65..8179c4169 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/FIFOCache.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/FIFOCache.java @@ -83,7 +83,7 @@ public class FIFOCache extends StampedCache { // 清理结束后依旧是满的,则删除第一个被缓存的对象 if (isFull() && null != first) { - removeWithoutLock(first.key, false); + removeWithoutLock(first.key); onRemove(first.key, first.obj); count++; } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/ReentrantCache.java b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/ReentrantCache.java index 7b1aa8ed7..22abbbf9a 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/ReentrantCache.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/ReentrantCache.java @@ -30,7 +30,6 @@ public abstract class ReentrantCache extends AbstractCache { private static final long serialVersionUID = 1L; // 一些特殊缓存,例如使用了LinkedHashMap的缓存,由于get方法也会改变Map的结构,导致无法使用读写锁 - // TODO 最优的解决方案是使用Guava的ConcurrentLinkedHashMap,此处使用简化的互斥锁 protected final ReentrantLock lock = new ReentrantLock(); @Override @@ -45,49 +44,12 @@ public abstract class ReentrantCache extends AbstractCache { @Override public boolean containsKey(final K key) { - lock.lock(); - try { - // 不存在或已移除 - final CacheObj co = getWithoutLock(key); - if (co == null) { - return false; - } - - if (!co.isExpired()) { - // 命中 - return true; - } - } finally { - lock.unlock(); - } - - // 过期 - remove(key, true); - return false; + return null != getOrRemoveExpired(key, false, false); } @Override public V get(final K key, final boolean isUpdateLastAccess) { - CacheObj co; - lock.lock(); - try { - co = getWithoutLock(key); - } finally { - lock.unlock(); - } - - // 未命中 - if (null == co) { - missCount.increment(); - return null; - } else if (!co.isExpired()) { - hitCount.increment(); - return co.get(isUpdateLastAccess); - } - - // 过期,既不算命中也不算非命中 - remove(key, true); - return null; + return getOrRemoveExpired(key, isUpdateLastAccess, true); } @Override @@ -114,7 +76,16 @@ public abstract class ReentrantCache extends AbstractCache { @Override public void remove(final K key) { - remove(key, false); + CacheObj co; + lock.lock(); + try { + co = removeWithoutLock(key); + } finally { + lock.unlock(); + } + if (null != co) { + onRemove(co.key, co.obj); + } } @Override @@ -138,21 +109,37 @@ public abstract class ReentrantCache extends AbstractCache { } /** - * 移除key对应的对象 - * - * @param key 键 - * @param withMissCount 是否计数丢失数 + * 获得值或清除过期值 + * @param key 键 + * @param isUpdateLastAccess 是否更新最后访问时间 + * @param isUpdateCount 是否更新计数器 + * @return 值或null */ - private void remove(final K key, final boolean withMissCount) { + private V getOrRemoveExpired(final K key, final boolean isUpdateLastAccess, final boolean isUpdateCount) { CacheObj co; lock.lock(); try { - co = removeWithoutLock(key, withMissCount); + co = getWithoutLock(key); + if(null != co && co.isExpired()){ + //过期移除 + removeWithoutLock(key); + co = null; + } } finally { lock.unlock(); } - if (null != co) { - onRemove(co.key, co.obj); + + // 未命中 + if (null == co) { + if(isUpdateCount){ + missCount.increment(); + } + return null; } + + if(isUpdateCount){ + hitCount.increment(); + } + return co.get(isUpdateLastAccess); } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/StampedCache.java b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/StampedCache.java index f05398b08..f1cc3a34c 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/StampedCache.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/cache/impl/StampedCache.java @@ -45,54 +45,12 @@ public abstract class StampedCache extends AbstractCache{ @Override public boolean containsKey(final K key) { - final long stamp = lock.readLock(); - try { - // 不存在或已移除 - final CacheObj co = getWithoutLock(key); - if (co == null) { - return false; - } - - if (!co.isExpired()) { - // 命中 - return true; - } - } finally { - lock.unlockRead(stamp); - } - - // 过期 - remove(key, true); - return false; + return null != get(key, false, false); } @Override public V get(final K key, final boolean isUpdateLastAccess) { - // 尝试读取缓存,使用乐观读锁 - long stamp = lock.tryOptimisticRead(); - CacheObj co = getWithoutLock(key); - if(!lock.validate(stamp)){ - // 有写线程修改了此对象,悲观读 - stamp = lock.readLock(); - try { - co = getWithoutLock(key); - } finally { - lock.unlockRead(stamp); - } - } - - // 未命中 - if (null == co) { - missCount.increment(); - return null; - } else if (!co.isExpired()) { - hitCount.increment(); - return co.get(isUpdateLastAccess); - } - - // 过期,既不算命中也不算非命中 - remove(key, true); - return null; + return get(key, isUpdateLastAccess, true); } @Override @@ -119,7 +77,16 @@ public abstract class StampedCache extends AbstractCache{ @Override public void remove(final K key) { - remove(key, false); + final long stamp = lock.writeLock(); + CacheObj co; + try { + co = removeWithoutLock(key); + } finally { + lock.unlockWrite(stamp); + } + if (null != co) { + onRemove(co.key, co.obj); + } } @Override @@ -133,21 +100,78 @@ public abstract class StampedCache extends AbstractCache{ } /** - * 移除key对应的对象 + * 获取值 + * + * @param key 键 + * @param isUpdateLastAccess 是否更新最后修改时间 + * @param isUpdateCount 是否更新命中数,get时更新,contains时不更新 + * @return 值或null + */ + private V get(final K key, final boolean isUpdateLastAccess, final boolean isUpdateCount) { + // 尝试读取缓存,使用乐观读锁 + long stamp = lock.tryOptimisticRead(); + CacheObj co = getWithoutLock(key); + if (false == lock.validate(stamp)) { + // 有写线程修改了此对象,悲观读 + stamp = lock.readLock(); + try { + co = getWithoutLock(key); + } finally { + lock.unlockRead(stamp); + } + } + + // 未命中 + if (null == co) { + if (isUpdateCount) { + missCount.increment(); + } + return null; + } else if (false == co.isExpired()) { + if (isUpdateCount) { + hitCount.increment(); + } + return co.get(isUpdateLastAccess); + } + + // 悲观锁,二次检查 + return getOrRemoveExpired(key, isUpdateCount); + } + + /** + * 同步获取值,如果过期则移除之 * * @param key 键 - * @param withMissCount 是否计数丢失数 + * @param isUpdateCount 是否更新命中数,get时更新,contains时不更新 + * @return 有效值或null */ - private void remove(final K key, final boolean withMissCount) { + private V getOrRemoveExpired(final K key, final boolean isUpdateCount) { final long stamp = lock.writeLock(); CacheObj co; try { - co = removeWithoutLock(key, withMissCount); + co = getWithoutLock(key); + if (null == co) { + return null; + } + if (false == co.isExpired()) { + // 首先尝试获取值,如果值存在且有效,返回之 + if (isUpdateCount) { + hitCount.increment(); + } + return co.getValue(); + } + + // 无效移除 + co = removeWithoutLock(key); + if(isUpdateCount){ + missCount.increment(); + } } finally { lock.unlockWrite(stamp); } if (null != co) { onRemove(co.key, co.obj); } + return null; } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/cache/IssueI8MEIXTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/cache/IssueI8MEIXTest.java new file mode 100644 index 000000000..bcb23c03a --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/cache/IssueI8MEIXTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.cache; + +import org.dromara.hutool.core.cache.impl.TimedCache; +import org.dromara.hutool.core.lang.Console; +import org.dromara.hutool.core.thread.ThreadUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * https://gitee.com/dromara/hutool/issues/I8MEIX
+ * get操作非原子 + */ +public class IssueI8MEIXTest { + @Test + @Disabled + void getRemoveTest() { + final TimedCache cache = new TimedCache<>(200); + cache.put("a", "123"); + + ThreadUtil.sleep(300); + + // 测试时,在get后的remove前加sleep测试在读取过程中put新值的问题 + ThreadUtil.execute(() -> { + Console.log("get begin."); + Console.log(cache.get("a")); + }); + + ThreadUtil.execute(() -> { + ThreadUtil.sleep(200); + cache.put("a", "456"); + Console.log("put ok."); + }); + + ThreadUtil.sleep(1000); + } +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/IssueI5Q4HDTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/IssueI5Q4HDTest.java index a6f48e27d..9301f8bfe 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/IssueI5Q4HDTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/IssueI5Q4HDTest.java @@ -32,6 +32,6 @@ public class IssueI5Q4HDTest { wordTree.addWords(keyWordSet); //DateUtil.beginOfHour() final List strings = wordTree.matchAll(content, -1, true, true); - Assertions.assertEquals("[站房, 站房建设, 面积较小, 不符合规范要求, 辅助设施, 站房]", strings.toString()); + Assertions.assertEquals("[站房建设, 面积较小, 不符合规范要求, 辅助设施, 站房]", strings.toString()); } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/NFATest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/NFATest.java index 5e48b276a..c17a186d8 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/NFATest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/text/dfa/NFATest.java @@ -53,7 +53,7 @@ public class NFATest { stopWatch.start("wordtree_char_find"); final List ans2 = wordTree.matchAll(input, -1, true, true); stopWatch.stop(); - Assertions.assertEquals("she,he,her,say", String.join(",", ans2)); + Assertions.assertEquals("she,her,say", String.join(",", ans2)); //Console.log(stopWatch.prettyPrint()); } @@ -120,7 +120,7 @@ public class NFATest { wordTreeLocal.addWords("say", "her", "he", "she", "shr"); final List ans2 = wordTreeLocal.matchAll(input, -1, true, true); stopWatch.stop(); - Assertions.assertEquals("she,he,her,say", String.join(",", ans2)); + Assertions.assertEquals("she,her,say", String.join(",", ans2)); //Console.log(stopWatch.prettyPrint()); } @@ -157,8 +157,8 @@ public class NFATest { final List result1 = wordTreeLocal.matchAll(input, -1, true, true); stopWatch.stop(); - Assertions.assertEquals(3, result1.size()); - Assertions.assertEquals("赵,赵啊,赵啊三", String.join(",", result1)); + Assertions.assertEquals(1, result1.size()); + Assertions.assertEquals("赵啊三", String.join(",", result1)); //Console.log(stopWatch.prettyPrint()); } @@ -196,8 +196,8 @@ public class NFATest { .collect(Collectors.toList()); stopWatch.stop(); - Assertions.assertEquals(3, result1.size()); - Assertions.assertEquals("赵,赵啊,赵啊三", String.join(",", result1)); + Assertions.assertEquals(1, result1.size()); + Assertions.assertEquals("赵啊三", String.join(",", result1)); //Console.log(stopWatch.prettyPrint()); } @@ -233,7 +233,7 @@ public class NFATest { stopWatch.stop(); Assertions.assertEquals(1, result1.size()); - Assertions.assertEquals("赵", String.join(",", result1)); + Assertions.assertEquals("赵啊三", String.join(",", result1)); //Console.log(stopWatch.prettyPrint()); }