3 changed files with 150 additions and 105 deletions
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.util; |
||||
|
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.ConcurrentLinkedDeque; |
||||
import java.util.concurrent.locks.ReadWriteLock; |
||||
import java.util.concurrent.locks.ReentrantReadWriteLock; |
||||
import java.util.function.Function; |
||||
|
||||
/** |
||||
* Simple LRU (Least Recently Used) cache, bounded by a specified cache limit. |
||||
* |
||||
* <p>This implementation is backed by a {@code ConcurrentHashMap} for storing |
||||
* the cached values and a {@code ConcurrentLinkedDeque} for ordering the keys |
||||
* and choosing the least recently used key when the cache is at full capacity. |
||||
* |
||||
* @author Brian Clozel |
||||
* @author Juergen Hoeller |
||||
* @since 5.3 |
||||
* @param <K> the type of the key used for cache retrieval |
||||
* @param <V> the type of the cached values |
||||
* @see #get |
||||
*/ |
||||
public class ConcurrentLruCache<K, V> { |
||||
|
||||
private final int sizeLimit; |
||||
|
||||
private final Function<K, V> generator; |
||||
|
||||
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>(); |
||||
|
||||
private final ConcurrentLinkedDeque<K> queue = new ConcurrentLinkedDeque<>(); |
||||
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock(); |
||||
|
||||
private volatile int size; |
||||
|
||||
|
||||
/** |
||||
* Create a new cache instance with the given limit and generator function. |
||||
* @param sizeLimit the maximum number of entries in the cache |
||||
* (0 indicates no caching, always generating a new value) |
||||
* @param generator a function to generate a new value for a given key |
||||
*/ |
||||
public ConcurrentLruCache(int sizeLimit, Function<K, V> generator) { |
||||
Assert.isTrue(sizeLimit >= 0, "Cache size limit must not be negative"); |
||||
Assert.notNull(generator, "Generator function must not be null"); |
||||
this.sizeLimit = sizeLimit; |
||||
this.generator = generator; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Retrieve an entry from the cache, potentially triggering generation |
||||
* of the value. |
||||
* @param key the key to retrieve the entry for |
||||
* @return the cached or newly generated value |
||||
*/ |
||||
public V get(K key) { |
||||
if (this.sizeLimit == 0) { |
||||
return this.generator.apply(key); |
||||
} |
||||
|
||||
V cached = this.cache.get(key); |
||||
if (cached != null) { |
||||
if (this.size < this.sizeLimit) { |
||||
return cached; |
||||
} |
||||
this.lock.readLock().lock(); |
||||
try { |
||||
if (this.queue.removeLastOccurrence(key)) { |
||||
this.queue.offer(key); |
||||
} |
||||
return cached; |
||||
} |
||||
finally { |
||||
this.lock.readLock().unlock(); |
||||
} |
||||
} |
||||
|
||||
this.lock.writeLock().lock(); |
||||
try { |
||||
// Retrying in case of concurrent reads on the same key
|
||||
cached = this.cache.get(key); |
||||
if (cached != null) { |
||||
if (this.queue.removeLastOccurrence(key)) { |
||||
this.queue.offer(key); |
||||
} |
||||
return cached; |
||||
} |
||||
// Generate value first, to prevent size inconsistency
|
||||
V value = this.generator.apply(key); |
||||
int cacheSize = this.size; |
||||
if (cacheSize == this.sizeLimit) { |
||||
K leastUsed = this.queue.poll(); |
||||
if (leastUsed != null) { |
||||
this.cache.remove(leastUsed); |
||||
cacheSize--; |
||||
} |
||||
} |
||||
this.queue.offer(key); |
||||
this.cache.put(key, value); |
||||
this.size = cacheSize + 1; |
||||
return value; |
||||
} |
||||
finally { |
||||
this.lock.writeLock().unlock(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the current size of the cache. |
||||
* @see #sizeLimit() |
||||
*/ |
||||
public int size() { |
||||
return this.size; |
||||
} |
||||
|
||||
/** |
||||
* Return the the maximum number of entries in the cache |
||||
* (0 indicates no caching, always generating a new value). |
||||
* @see #size() |
||||
*/ |
||||
public int sizeLimit() { |
||||
return this.sizeLimit; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue