diff --git a/CHANGELOG.md b/CHANGELOG.md
index baa866a4b..8cde66ed3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
-------------------------------------------------------------------------------------------------------------
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java
index 6999922ee..6bb549689 100644
--- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java
@@ -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;
/**
* 超时和限制大小的缓存的默认实现
@@ -31,11 +29,6 @@ public abstract class AbstractCache implements Cache {
protected Map> 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 implements Cache {
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 implements Cache {
* @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 co = new CacheObj<>(key, object, timeout);
if (timeout != 0) {
existCustomTimeout = true;
@@ -106,29 +89,6 @@ public abstract class AbstractCache implements Cache {
// ---------------------------------------------------------------- put end
// ---------------------------------------------------------------- get start
- @Override
- public boolean containsKey(K key) {
- final long stamp = lock.readLock();
- try {
- // 不存在或已移除
- final CacheObj 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 implements Cache {
}
return v;
}
-
- @Override
- public V get(K key, boolean isUpdateLastAccess) {
- // 尝试读取缓存,使用乐观读锁
- long stamp = lock.tryOptimisticRead();
- CacheObj 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 implements Cache {
CacheObjIterator copiedIterator = (CacheObjIterator) this.cacheObjIterator();
return new CacheValuesIterator<>(copiedIterator);
}
-
- @Override
- public Iterator> cacheObjIterator() {
- CopiedIter> copiedIterator;
- final long stamp = lock.readLock();
- try {
- copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
- } finally {
- lock.unlockRead(stamp);
- }
- return new CacheObjIterator<>(copiedIterator);
- }
-
// ---------------------------------------------------------------- prune start
-
/**
* 清理实现
* 子类实现此方法时无需加锁
@@ -229,16 +145,6 @@ public abstract class AbstractCache implements Cache {
* @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 implements Cache {
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 implements Cache {
}
}
- /**
- * 移除key对应的对象
- *
- * @param key 键
- * @param withMissCount 是否计数丢失数
- */
- private void remove(K key, boolean withMissCount) {
- final long stamp = lock.writeLock();
- CacheObj 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 implements Cache {
* @param withMissCount 是否计数丢失数
* @return 移除的对象,无返回null
*/
- private CacheObj removeWithoutLock(K key, boolean withMissCount) {
+ protected CacheObj removeWithoutLock(K key, boolean withMissCount) {
final CacheObj co = cacheMap.remove(key);
if (withMissCount) {
// 在丢失计数有效的情况下,移除一般为get时的超时操作,此处应该丢失数+1
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java
index c403ae10c..558fed75f 100644
--- a/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java
@@ -16,7 +16,7 @@ import java.util.LinkedHashMap;
* @param 值类型
* @author Looly
*/
-public class FIFOCache extends AbstractCache {
+public class FIFOCache extends StampedCache {
private static final long serialVersionUID = 1L;
/**
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java
index 32709ed6b..4b1e2608a 100644
--- a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java
@@ -15,7 +15,7 @@ import java.util.Iterator;
* @param 键类型
* @param 值类型
*/
-public class LFUCache extends AbstractCache {
+public class LFUCache extends StampedCache {
private static final long serialVersionUID = 1L;
/**
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java
index b385b57fe..29f83f02f 100644
--- a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java
@@ -16,7 +16,7 @@ import java.util.Iterator;
* @param 键类型
* @param 值类型
*/
-public class LRUCache extends AbstractCache {
+public class LRUCache extends ReentrantCache {
private static final long serialVersionUID = 1L;
/**
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java
new file mode 100644
index 000000000..180bc7785
--- /dev/null
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/ReentrantCache.java
@@ -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无法使用读写锁的问题
+ * 例如使用了LinkedHashMap的缓存,由于get方法也会改变Map的结构,因此读写必须加互斥锁
+ *
+ * @param 键类型
+ * @param 值类型
+ * @author looly
+ * @since 5.7.15
+ */
+public abstract class ReentrantCache extends AbstractCache {
+ 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 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 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> cacheObjIterator() {
+ CopiedIter> 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 co;
+ try {
+ co = removeWithoutLock(key, withMissCount);
+ } finally {
+ lock.unlock();
+ }
+ if (null != co) {
+ onRemove(co.key, co.obj);
+ }
+ }
+}
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java
new file mode 100644
index 000000000..79534f2bf
--- /dev/null
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/StampedCache.java
@@ -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 键类型
+ * @param 值类型
+ * @author looly
+ * @since 5.7.15
+ */
+public abstract class StampedCache extends AbstractCache{
+ 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 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 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> cacheObjIterator() {
+ CopiedIter> 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 co;
+ try {
+ co = removeWithoutLock(key, withMissCount);
+ } finally {
+ lock.unlockWrite(stamp);
+ }
+ if (null != co) {
+ onRemove(co.key, co.obj);
+ }
+ }
+}
diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java
index 5e094875d..e03ede728 100644
--- a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java
+++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java
@@ -16,7 +16,7 @@ import java.util.concurrent.ScheduledFuture;
* @param 键类型
* @param 值类型
*/
-public class TimedCache extends AbstractCache {
+public class TimedCache extends StampedCache {
private static final long serialVersionUID = 1L;
/** 正在执行的定时任务 */
diff --git a/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java
new file mode 100644
index 000000000..6ccc2db2f
--- /dev/null
+++ b/hutool-cache/src/test/java/cn/hutool/cache/LRUCacheTest.java
@@ -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
+ * 并发问题测试,在5.7.15前,LRUCache存在并发问题,多线程get后,map结构变更,导致null的位置不确定,
+ * 并可能引起死锁。
+ */
+public class LRUCacheTest {
+
+ @Test
+ public void readWriteTest() throws InterruptedException {
+ LRUCache 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());
+ }
+}