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()); + } +}