SimpleLruCache.java
/*
* Copyright (C) 2019, Marc Strapetz <marc.strapetz@syntevo.com>
* Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.util;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.internal.JGitText;
/**
* Simple limited size cache based on ConcurrentHashMap purging entries in LRU
* order when reaching size limit
*
* @param <K>
* the type of keys maintained by this cache
* @param <V>
* the type of mapped values
*
* @since 5.1.9
*/
public class SimpleLruCache<K, V> {
private static class Entry<K, V> {
private final K key;
private final V value;
// pseudo clock timestamp of the last access to this entry
private volatile long lastAccessed;
private long lastAccessedSorting;
Entry(K key, V value, long lastAccessed) {
this.key = key;
this.value = value;
this.lastAccessed = lastAccessed;
}
void copyAccessTime() {
lastAccessedSorting = lastAccessed;
}
@SuppressWarnings("nls")
@Override
public String toString() {
return "Entry [lastAccessed=" + lastAccessed + ", key=" + key
+ ", value=" + value + "]";
}
}
private Lock lock = new ReentrantLock();
private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>();
private volatile int maximumSize;
private int purgeSize;
// pseudo clock to implement LRU order of access to entries
private volatile long time = 0L;
private static void checkPurgeFactor(float purgeFactor) {
if (purgeFactor <= 0 || purgeFactor >= 1) {
throw new IllegalArgumentException(
MessageFormat.format(JGitText.get().invalidPurgeFactor,
Float.valueOf(purgeFactor)));
}
}
private static int purgeSize(int maxSize, float purgeFactor) {
return (int) ((1 - purgeFactor) * maxSize);
}
/**
* Create a new cache
*
* @param maxSize
* maximum size of the cache, to reduce need for synchronization
* this is not a hard limit. The real size of the cache could be
* slightly above this maximum if multiple threads put new values
* concurrently
* @param purgeFactor
* when the size of the map reaches maxSize the oldest entries
* will be purged to free up some space for new entries,
* {@code purgeFactor} is the fraction of {@code maxSize} to
* purge when this happens
*/
public SimpleLruCache(int maxSize, float purgeFactor) {
checkPurgeFactor(purgeFactor);
this.maximumSize = maxSize;
this.purgeSize = purgeSize(maxSize, purgeFactor);
}
/**
* Returns the value to which the specified key is mapped, or {@code null}
* if this map contains no mapping for the key.
*
* <p>
* More formally, if this cache contains a mapping from a key {@code k} to a
* value {@code v} such that {@code key.equals(k)}, then this method returns
* {@code v}; otherwise it returns {@code null}. (There can be at most one
* such mapping.)
*
* @param key
* the key
*
* @throws NullPointerException
* if the specified key is null
*
* @return value mapped for this key, or {@code null} if no value is mapped
*/
@SuppressWarnings("NonAtomicVolatileUpdate")
public V get(Object key) {
Entry<K, V> entry = map.get(key);
if (entry != null) {
entry.lastAccessed = tick();
return entry.value;
}
return null;
}
/**
* Maps the specified key to the specified value in this cache. Neither the
* key nor the value can be null.
*
* <p>
* The value can be retrieved by calling the {@code get} method with a key
* that is equal to the original key.
*
* @param key
* key with which the specified value is to be associated
* @param value
* value to be associated with the specified key
* @return the previous value associated with {@code key}, or {@code null}
* if there was no mapping for {@code key}
* @throws NullPointerException
* if the specified key or value is null
*/
@SuppressWarnings("NonAtomicVolatileUpdate")
public V put(@NonNull K key, @NonNull V value) {
map.put(key, new Entry<>(key, value, tick()));
if (map.size() > maximumSize) {
purge();
}
return value;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
private long tick() {
return ++time;
}
/**
* Returns the current size of this cache
*
* @return the number of key-value mappings in this cache
*/
public int size() {
return map.size();
}
/**
* Reconfigures the cache. If {@code maxSize} is reduced some entries will
* be purged.
*
* @param maxSize
* maximum size of the cache
*
* @param purgeFactor
* when the size of the map reaches maxSize the oldest entries
* will be purged to free up some space for new entries,
* {@code purgeFactor} is the fraction of {@code maxSize} to
* purge when this happens
*/
public void configure(int maxSize, float purgeFactor) {
lock.lock();
try {
checkPurgeFactor(purgeFactor);
this.maximumSize = maxSize;
this.purgeSize = purgeSize(maxSize, purgeFactor);
if (map.size() >= maximumSize) {
purge();
}
} finally {
lock.unlock();
}
}
private void purge() {
// don't try to compete if another thread already has the lock
if (lock.tryLock()) {
try {
List<Entry> entriesToPurge = new ArrayList<>(map.values());
// copy access times to avoid other threads interfere with
// sorting
for (Entry e : entriesToPurge) {
e.copyAccessTime();
}
Collections.sort(entriesToPurge,
Comparator.comparingLong(o -> -o.lastAccessedSorting));
for (int index = purgeSize; index < entriesToPurge
.size(); index++) {
map.remove(entriesToPurge.get(index).key);
}
} finally {
lock.unlock();
}
}
}
}