mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-04-19 03:01:48 +08:00
修复StampedCache的get方法非原子问题
This commit is contained in:
parent
dfc0b4ecf1
commit
6f4a115032
@ -262,16 +262,10 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
|
||||
* 移除key对应的对象,不加锁
|
||||
*
|
||||
* @param key 键
|
||||
* @param withMissCount 是否计数丢失数
|
||||
* @return 移除的对象,无返回null
|
||||
*/
|
||||
protected CacheObj<K, V> removeWithoutLock(final K key, final boolean withMissCount) {
|
||||
final CacheObj<K, V> co = cacheMap.remove(MutableObj.of(key));
|
||||
if (withMissCount) {
|
||||
// 在丢失计数有效的情况下,移除一般为get时的超时操作,此处应该丢失数+1
|
||||
this.missCount.increment();
|
||||
}
|
||||
return co;
|
||||
protected CacheObj<K, V> removeWithoutLock(final K key) {
|
||||
return cacheMap.remove(MutableObj.of(key));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +83,7 @@ public class FIFOCache<K, V> extends StampedCache<K, V> {
|
||||
|
||||
// 清理结束后依旧是满的,则删除第一个被缓存的对象
|
||||
if (isFull() && null != first) {
|
||||
removeWithoutLock(first.key, false);
|
||||
removeWithoutLock(first.key);
|
||||
onRemove(first.key, first.obj);
|
||||
count++;
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ public abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {
|
||||
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<K, V> extends AbstractCache<K, V> {
|
||||
|
||||
@Override
|
||||
public boolean containsKey(final K key) {
|
||||
lock.lock();
|
||||
try {
|
||||
// 不存在或已移除
|
||||
final CacheObj<K, V> 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<K, V> 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<K, V> extends AbstractCache<K, V> {
|
||||
|
||||
@Override
|
||||
public void remove(final K key) {
|
||||
remove(key, false);
|
||||
CacheObj<K, V> 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<K, V> extends AbstractCache<K, V> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除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<K, V> 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);
|
||||
}
|
||||
}
|
||||
|
@ -45,54 +45,12 @@ public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
|
||||
|
||||
@Override
|
||||
public boolean containsKey(final K key) {
|
||||
final long stamp = lock.readLock();
|
||||
try {
|
||||
// 不存在或已移除
|
||||
final CacheObj<K, V> 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<K, V> 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<K, V> extends AbstractCache<K, V>{
|
||||
|
||||
@Override
|
||||
public void remove(final K key) {
|
||||
remove(key, false);
|
||||
final long stamp = lock.writeLock();
|
||||
CacheObj<K, V> 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<K, V> extends AbstractCache<K, V>{
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除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<K, V> 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<K, V> 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;
|
||||
}
|
||||
}
|
||||
|
48
hutool-core/src/test/java/org/dromara/hutool/core/cache/IssueI8MEIXTest.java
vendored
Normal file
48
hutool-core/src/test/java/org/dromara/hutool/core/cache/IssueI8MEIXTest.java
vendored
Normal file
@ -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<br>
|
||||
* get操作非原子
|
||||
*/
|
||||
public class IssueI8MEIXTest {
|
||||
@Test
|
||||
@Disabled
|
||||
void getRemoveTest() {
|
||||
final TimedCache<String, String> 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);
|
||||
}
|
||||
}
|
@ -32,6 +32,6 @@ public class IssueI5Q4HDTest {
|
||||
wordTree.addWords(keyWordSet);
|
||||
//DateUtil.beginOfHour()
|
||||
final List<String> strings = wordTree.matchAll(content, -1, true, true);
|
||||
Assertions.assertEquals("[站房, 站房建设, 面积较小, 不符合规范要求, 辅助设施, 站房]", strings.toString());
|
||||
Assertions.assertEquals("[站房建设, 面积较小, 不符合规范要求, 辅助设施, 站房]", strings.toString());
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ public class NFATest {
|
||||
stopWatch.start("wordtree_char_find");
|
||||
final List<String> 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<String> 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<String> 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());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user