1
0
mirror of https://gitee.com/dromara/hutool.git synced 2025-04-05 17:37:59 +08:00

修复StampedCache的get方法非原子问题

This commit is contained in:
Looly 2023-12-09 01:05:55 +08:00
parent 9c351cfb84
commit 1e06dd4674
6 changed files with 144 additions and 109 deletions
CHANGELOG.md
hutool-cache/src

View File

@ -2,7 +2,7 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.24(2023-12-05)
# 5.8.24(2023-12-09)
### 🐣新特性
* 【cache 】 Cache增加get重载可自定义超时时间issue#I8G0DL@Gitee
@ -16,6 +16,7 @@
* 【http 】 修复RootAction send404 抛异常问题pr#1107@Gitee
* 【extra 】 修复Archiver 最后一个 Entry 为空文件夹时未关闭 Entry问题pr#1123@Gitee
* 【core 】 修复ImgUtil.convert png转jpg在jdk9+中失败问题issue#I8L8UA@Gitee
* 【cache 】 修复StampedCache的get方法非原子问题issue#I8MEIX@Gitee
-------------------------------------------------------------------------------------------------------------
# 5.8.23(2023-11-12)

View File

@ -254,16 +254,10 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* 移除key对应的对象不加锁
*
* @param key
* @param withMissCount 是否计数丢失数
* @return 移除的对象无返回null
*/
protected CacheObj<K, V> removeWithoutLock(K key, 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(K key) {
return cacheMap.remove(MutableObj.of(key));
}
/**

View File

@ -71,7 +71,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++;
}

View File

@ -33,49 +33,12 @@ public abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {
@Override
public boolean containsKey(K key) {
lock.lock();
try {
// 不存在或已移除
final CacheObj<K, V> co = getWithoutLock(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlock();
}
// 过期
remove(key, true);
return false;
return null != getOrRemoveExpired(key, false, false);
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
CacheObj<K, V> co;
lock.lock();
try {
co = getWithoutLock(key);
} finally {
lock.unlock();
}
// 未命中
if (null == co) {
missCount.increment();
return null;
} else if (false == co.isExpired()) {
hitCount.increment();
return co.get(isUpdateLastAccess);
}
// 过期既不算命中也不算非命中
remove(key, true);
return null;
return getOrRemoveExpired(key, isUpdateLastAccess, true);
}
@Override
@ -102,7 +65,16 @@ public abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {
@Override
public void remove(K key) {
remove(key, false);
lock.lock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key);
} finally {
lock.unlock();
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
@Override
@ -126,21 +98,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(K key, boolean withMissCount) {
lock.lock();
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);
}
}

View File

@ -1,6 +1,7 @@
package cn.hutool.cache.impl;
import cn.hutool.core.collection.CopiedIter;
import cn.hutool.core.thread.ThreadUtil;
import java.util.Iterator;
import java.util.concurrent.locks.StampedLock;
@ -13,7 +14,7 @@ import java.util.concurrent.locks.StampedLock;
* @author looly
* @since 5.7.15
*/
public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
public abstract class StampedCache<K, V> extends AbstractCache<K, V> {
private static final long serialVersionUID = 1L;
// 乐观锁此处使用乐观锁解决读多写少的场景
@ -33,54 +34,12 @@ public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
@Override
public boolean containsKey(K key) {
final long stamp = lock.readLock();
try {
// 不存在或已移除
final CacheObj<K, V> co = getWithoutLock(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlockRead(stamp);
}
// 过期
remove(key, true);
return false;
return null != get(key, false, false);
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
// 尝试读取缓存使用乐观读锁
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) {
missCount.increment();
return null;
} else if (false == co.isExpired()) {
hitCount.increment();
return co.get(isUpdateLastAccess);
}
// 过期既不算命中也不算非命中
remove(key, true);
return null;
return get(key, isUpdateLastAccess, true);
}
@Override
@ -107,7 +66,16 @@ public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
@Override
public void remove(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
@ -121,21 +89,75 @@ public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
}
/**
* 移除key对应的对象
* 获取值
*
* @param key
* @param isUpdateLastAccess 是否更新最后修改时间
* @param isUpdateCount 是否更新命中数get时更新contains时不更新
* @return 值或null
*/
private V get(K key, boolean isUpdateLastAccess, 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(K key, boolean withMissCount) {
private V getOrRemoveExpired(K key, 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);
} finally {
lock.unlockWrite(stamp);
}
if (null != co) {
onRemove(co.key, co.obj);
}
return null;
}
}

View File

@ -0,0 +1,30 @@
package cn.hutool.cache;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.lang.Console;
import cn.hutool.core.thread.ThreadUtil;
import org.junit.Ignore;
import org.junit.Test;
public class IssueI8MEIXTest {
@Test
@Ignore
public 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(cache.get("a"));
});
ThreadUtil.execute(()->{
cache.put("a", "456");
});
ThreadUtil.sleep(1000);
}
}