This commit is contained in:
Looly 2021-10-19 00:27:10 +08:00
parent f966fda28f
commit 3e36d7fd21
9 changed files with 337 additions and 135 deletions

View File

@ -3,7 +3,7 @@
-------------------------------------------------------------------------------------------------------------
# 5.7.15 (2021-10-18)
# 5.7.15 (2021-10-19)
### 🐣新特性
* 【db 】 Db.quietSetAutoCommit增加判空issue#I4D75B@Gitee
@ -18,6 +18,7 @@
* 【poi 】 修复ExcelWriter多余调试信息导致的问题issue#1884@Github
* 【poi 】 修复TemporalAccessorUtil.toInstant使用DateTimeFormatter导致问题issue#1891@Github
* 【poi 】 修复sheet.getRow(y)为null导致的问题issue#1893@Github
* 【cache 】 修复LRUCache线程安全问题issue#1895@Github
-------------------------------------------------------------------------------------------------------------

View File

@ -2,7 +2,6 @@ package cn.hutool.cache.impl;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheListener;
import cn.hutool.core.collection.CopiedIter;
import cn.hutool.core.lang.func.Func0;
import java.util.Iterator;
@ -12,7 +11,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.StampedLock;
/**
* 超时和限制大小的缓存的默认实现<br>
@ -31,11 +29,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
protected Map<K, CacheObj<K, V>> cacheMap;
// 乐观锁此处使用乐观锁解决读多写少的场景
// get时乐观读再检查是否修改修改则转入悲观读重新读一遍可以有效解决在写时阻塞大量读操作的情况
// see: https://www.cnblogs.com/jiagoushijuzi/p/13721319.html
protected final StampedLock lock = new StampedLock();
/**
* 写的时候每个key一把锁降低锁的粒度
*/
@ -75,16 +68,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
put(key, object, timeout);
}
@Override
public void put(K key, V object, long timeout) {
final long stamp = lock.writeLock();
try {
putWithoutLock(key, object, timeout);
} finally {
lock.unlockWrite(stamp);
}
}
/**
* 加入元素无锁
*
@ -93,7 +76,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* @param timeout 超时时长
* @since 4.5.16
*/
private void putWithoutLock(K key, V object, long timeout) {
protected void putWithoutLock(K key, V object, long timeout) {
CacheObj<K, V> co = new CacheObj<>(key, object, timeout);
if (timeout != 0) {
existCustomTimeout = true;
@ -106,29 +89,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
// ---------------------------------------------------------------- put end
// ---------------------------------------------------------------- get start
@Override
public boolean containsKey(K key) {
final long stamp = lock.readLock();
try {
// 不存在或已移除
final CacheObj<K, V> co = cacheMap.get(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlockRead(stamp);
}
// 过期
remove(key, true);
return false;
}
/**
* @return 命中数
*/
@ -170,36 +130,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
}
return v;
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
// 尝试读取缓存使用乐观读锁
long stamp = lock.tryOptimisticRead();
CacheObj<K, V> co = cacheMap.get(key);
if(false == lock.validate(stamp)){
// 有写线程修改了此对象悲观读
stamp = lock.readLock();
try {
co = cacheMap.get(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;
}
// ---------------------------------------------------------------- get end
@Override
@ -207,21 +137,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
CacheObjIterator<K, V> copiedIterator = (CacheObjIterator<K, V>) this.cacheObjIterator();
return new CacheValuesIterator<>(copiedIterator);
}
@Override
public Iterator<CacheObj<K, V>> cacheObjIterator() {
CopiedIter<CacheObj<K, V>> copiedIterator;
final long stamp = lock.readLock();
try {
copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
} finally {
lock.unlockRead(stamp);
}
return new CacheObjIterator<>(copiedIterator);
}
// ---------------------------------------------------------------- prune start
/**
* 清理实现<br>
* 子类实现此方法时无需加锁
@ -229,16 +145,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* @return 清理数
*/
protected abstract int pruneCache();
@Override
public final int prune() {
final long stamp = lock.writeLock();
try {
return pruneCache();
} finally {
lock.unlockWrite(stamp);
}
}
// ---------------------------------------------------------------- prune end
// ---------------------------------------------------------------- common start
@ -270,21 +176,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
return (capacity > 0) && (cacheMap.size() >= capacity);
}
@Override
public void remove(K key) {
remove(key, false);
}
@Override
public void clear() {
final long stamp = lock.writeLock();
try {
cacheMap.clear();
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public int size() {
return cacheMap.size();
@ -338,25 +229,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
}
}
/**
* 移除key对应的对象
*
* @param key
* @param withMissCount 是否计数丢失数
*/
private void remove(K key, boolean withMissCount) {
final long stamp = lock.writeLock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key, withMissCount);
} finally {
lock.unlockWrite(stamp);
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
/**
* 移除key对应的对象不加锁
*
@ -364,7 +236,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* @param withMissCount 是否计数丢失数
* @return 移除的对象无返回null
*/
private CacheObj<K, V> removeWithoutLock(K key, boolean withMissCount) {
protected CacheObj<K, V> removeWithoutLock(K key, boolean withMissCount) {
final CacheObj<K, V> co = cacheMap.remove(key);
if (withMissCount) {
// 在丢失计数有效的情况下移除一般为get时的超时操作此处应该丢失数+1

View File

@ -16,7 +16,7 @@ import java.util.LinkedHashMap;
* @param <V> 值类型
* @author Looly
*/
public class FIFOCache<K, V> extends AbstractCache<K, V> {
public class FIFOCache<K, V> extends StampedCache<K, V> {
private static final long serialVersionUID = 1L;
/**

View File

@ -15,7 +15,7 @@ import java.util.Iterator;
* @param <K> 键类型
* @param <V> 值类型
*/
public class LFUCache<K, V> extends AbstractCache<K, V> {
public class LFUCache<K, V> extends StampedCache<K, V> {
private static final long serialVersionUID = 1L;
/**

View File

@ -16,7 +16,7 @@ import java.util.Iterator;
* @param <K> 键类型
* @param <V> 值类型
*/
public class LRUCache<K, V> extends AbstractCache<K, V> {
public class LRUCache<K, V> extends ReentrantCache<K, V> {
private static final long serialVersionUID = 1L;
/**

View File

@ -0,0 +1,136 @@
package cn.hutool.cache.impl;
import cn.hutool.core.collection.CopiedIter;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用{@link ReentrantLock}保护的缓存读写都使用悲观锁完成主要避免某些Map无法使用读写锁的问题<br>
* 例如使用了LinkedHashMap的缓存由于get方法也会改变Map的结构因此读写必须加互斥锁
*
* @param <K> 键类型
* @param <V> 值类型
* @author looly
* @since 5.7.15
*/
public abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {
private static final long serialVersionUID = 1L;
// 一些特殊缓存例如使用了LinkedHashMap的缓存由于get方法也会改变Map的结构导致无法使用读写锁
// 最优的解决方案是使用Guava的ConcurrentLinkedHashMap此处使用简化的互斥锁
protected final ReentrantLock lock = new ReentrantLock();
@Override
public void put(K key, V object, long timeout) {
lock.lock();
try {
putWithoutLock(key, object, timeout);
} finally {
lock.unlock();
}
}
@Override
public boolean containsKey(K key) {
lock.lock();
try {
// 不存在或已移除
final CacheObj<K, V> co = cacheMap.get(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlock();
}
// 过期
remove(key, true);
return false;
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
CacheObj<K, V> co;
lock.lock();
try {
co = cacheMap.get(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;
}
@Override
public Iterator<CacheObj<K, V>> cacheObjIterator() {
CopiedIter<CacheObj<K, V>> copiedIterator;
lock.lock();
try {
copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
} finally {
lock.unlock();
}
return new CacheObjIterator<>(copiedIterator);
}
@Override
public final int prune() {
lock.lock();
try {
return pruneCache();
} finally {
lock.unlock();
}
}
@Override
public void remove(K key) {
remove(key, false);
}
@Override
public void clear() {
lock.lock();
try {
cacheMap.clear();
} finally {
lock.unlock();
}
}
/**
* 移除key对应的对象
*
* @param key
* @param withMissCount 是否计数丢失数
*/
private void remove(K key, boolean withMissCount) {
lock.lock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key, withMissCount);
} finally {
lock.unlock();
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
}

View File

@ -0,0 +1,141 @@
package cn.hutool.cache.impl;
import cn.hutool.core.collection.CopiedIter;
import java.util.Iterator;
import java.util.concurrent.locks.StampedLock;
/**
* 使用{@link StampedLock}保护的缓存使用读写乐观锁
*
* @param <K> 键类型
* @param <V> 值类型
* @author looly
* @since 5.7.15
*/
public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
private static final long serialVersionUID = 1L;
// 乐观锁此处使用乐观锁解决读多写少的场景
// get时乐观读再检查是否修改修改则转入悲观读重新读一遍可以有效解决在写时阻塞大量读操作的情况
// see: https://www.cnblogs.com/jiagoushijuzi/p/13721319.html
protected final StampedLock lock = new StampedLock();
@Override
public void put(K key, V object, long timeout) {
final long stamp = lock.writeLock();
try {
putWithoutLock(key, object, timeout);
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public boolean containsKey(K key) {
final long stamp = lock.readLock();
try {
// 不存在或已移除
final CacheObj<K, V> co = cacheMap.get(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlockRead(stamp);
}
// 过期
remove(key, true);
return false;
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
// 尝试读取缓存使用乐观读锁
long stamp = lock.tryOptimisticRead();
CacheObj<K, V> co = cacheMap.get(key);
if(false == lock.validate(stamp)){
// 有写线程修改了此对象悲观读
stamp = lock.readLock();
try {
co = cacheMap.get(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;
}
@Override
public Iterator<CacheObj<K, V>> cacheObjIterator() {
CopiedIter<CacheObj<K, V>> copiedIterator;
final long stamp = lock.readLock();
try {
copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
} finally {
lock.unlockRead(stamp);
}
return new CacheObjIterator<>(copiedIterator);
}
@Override
public final int prune() {
final long stamp = lock.writeLock();
try {
return pruneCache();
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public void remove(K key) {
remove(key, false);
}
@Override
public void clear() {
final long stamp = lock.writeLock();
try {
cacheMap.clear();
} finally {
lock.unlockWrite(stamp);
}
}
/**
* 移除key对应的对象
*
* @param key
* @param withMissCount 是否计数丢失数
*/
private void remove(K key, boolean withMissCount) {
final long stamp = lock.writeLock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key, withMissCount);
} finally {
lock.unlockWrite(stamp);
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
}

View File

@ -16,7 +16,7 @@ import java.util.concurrent.ScheduledFuture;
* @param <K> 键类型
* @param <V> 值类型
*/
public class TimedCache<K, V> extends AbstractCache<K, V> {
public class TimedCache<K, V> extends StampedCache<K, V> {
private static final long serialVersionUID = 1L;
/** 正在执行的定时任务 */

View File

@ -0,0 +1,52 @@
package cn.hutool.cache;
import cn.hutool.cache.impl.LRUCache;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
/**
* https://github.com/dromara/hutool/issues/1895<br>
* 并发问题测试在5.7.15前LRUCache存在并发问题多线程get后map结构变更导致null的位置不确定
* 并可能引起死锁
*/
public class LRUCacheTest {
@Test
public void readWriteTest() throws InterruptedException {
LRUCache<Integer, Integer> cache = CacheUtil.newLRUCache(10);
for (int i = 0; i < 10; i++) {
cache.put(i, i);
}
CountDownLatch countDownLatch = new CountDownLatch(10);
// 10个线程分别读0-9 10000次
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
cache.get(finalI);
}
countDownLatch.countDown();
}).start();
}
// 等待读线程结束
countDownLatch.await();
// 按顺序读0-9
StringBuilder sb1 = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb1.append(cache.get(i));
}
Assert.assertEquals("0123456789", sb1.toString());
// 新加11此时0最久未使用应该淘汰0
cache.put(11, 11);
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb2.append(cache.get(i));
}
Assert.assertEquals("null123456789", sb2.toString());
}
}