|
|
@@ -0,0 +1,282 @@
|
|
|
+/*
|
|
|
+ * @author Ranides Atterwim <ranides@gmail.com>
|
|
|
+ * @copyright Ranides Atterwim
|
|
|
+ * @license WTFPL
|
|
|
+ * @url http://ranides.net/projects/assira
|
|
|
+ */
|
|
|
+package net.ranides.assira.collection.maps;
|
|
|
+
|
|
|
+import java.lang.ref.Reference;
|
|
|
+import java.lang.ref.ReferenceQueue;
|
|
|
+import java.lang.ref.SoftReference;
|
|
|
+import java.util.AbstractMap;
|
|
|
+import java.util.ArrayDeque;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+/**
|
|
|
+ * <p>
|
|
|
+ * Kolekcja zaprojektowana jako podręczna pamięć na obiekty, których koszt
|
|
|
+ * utworzenia jest relatywnie wysoki. Kolekcja przechowuje {@code holdsize}
|
|
|
+ * ostatnio używanych elementów, bez względu na to, czy istnieją inne referencje
|
|
|
+ * do nich. Przez "użycie" rozumiemy dowolną operację zapis lub odczytu.
|
|
|
+ * W szczególności taka strategia pozwala polegać na metodzie "containsKey",
|
|
|
+ * która uchroni wartość przed natychmiastowym usunięciem - jeśli tylko stwierdzono,
|
|
|
+ * że klucz istnieje. Dodatkowo kolekcja przechowuje {@code cachesize} elementów
|
|
|
+ * za pomocą WeakReference - obiekty te mogą zostać zwolnione przez GC, jeśli
|
|
|
+ * zajdzie taka potrzeba.
|
|
|
+ * </p><p>
|
|
|
+ * Uwaga: wszystkie widoki zwracane przez "cache" są niebezpieczne w tym sensie,
|
|
|
+ * że nie można wywoływać <b>żadnej</b> metody na obiekcie "cache" w czasie
|
|
|
+ * iterowania po widoku. Jest to spowodowane tym, że cache uwzględnia czas dostępu
|
|
|
+ * do obiektów: nawet odczyt zmienia wewnętrzną reprezentację. W efekcie, jeśli
|
|
|
+ * podczas iteracji zostanie wywołana jakakolwiek inna metoda, to zostanie rzucone
|
|
|
+ * ConcurentModificationException.
|
|
|
+ * *
|
|
|
+ * @todo (assira #6) issue: holdlist contains duplicate entries
|
|
|
+ * if you invoke #get(key) x times, it will put x "key" entries inside list
|
|
|
+ * </p>
|
|
|
+ * <div class="message-note">thread-safe class</div>
|
|
|
+ * @param <K>
|
|
|
+ * @param <V>
|
|
|
+ * @author ranides
|
|
|
+ */
|
|
|
+@SuppressWarnings("element-type-mismatch")
|
|
|
+public class Cache<K,V> extends AbstractMap<K,V> {
|
|
|
+
|
|
|
+ private final CacheMap<K,V> map;
|
|
|
+
|
|
|
+ private final CacheList<V> list;
|
|
|
+
|
|
|
+ private final Map<K,V> view;
|
|
|
+
|
|
|
+ private final ReferenceQueue<V> queue = new ReferenceQueue<>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Tworzy cache, który przechowuje maksymalnie 1024 elementy, w tym 32
|
|
|
+ * ostatnio użyte są chronione przez usunięciem przez GC.
|
|
|
+ */
|
|
|
+ public Cache() {
|
|
|
+ this(1024, 32);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Tworzy cache, którzy przechowuje {@code cachesize} ostatnio używanych
|
|
|
+ * elementów, bez względu na to, czy istnieją do nich inne referencje.
|
|
|
+ * @param cachesize ilość przechowywanych elementów
|
|
|
+ */
|
|
|
+ public Cache(int cachesize) {
|
|
|
+ this(cachesize, cachesize);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Tworzy cache, którzy przechowuje {@code cachesize} ostatnio używanych
|
|
|
+ * elementów, w tym chroni przez usunięciem przez GC ostatnie {@code holdsize}
|
|
|
+ * elementów.
|
|
|
+ * @param cachesize ilość przechowywanych elementów
|
|
|
+ * @param holdsize ilość elementów chronionych przed GC
|
|
|
+ */
|
|
|
+ public Cache(int cachesize, int holdsize) {
|
|
|
+ if( cachesize < holdsize ) {
|
|
|
+ throw new IllegalArgumentException("cachesize must be greater or equal to holdsize");
|
|
|
+ }
|
|
|
+ this.list = new CacheList<>(Math.max(1,holdsize));
|
|
|
+ this.map = new CacheMap<>(cachesize);
|
|
|
+ this.view = MapUtils.map(map, (v)->asValue(v));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean containsKey(Object key) {
|
|
|
+ synchronized(this) {
|
|
|
+ return null != get(key);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public V get(Object key) {
|
|
|
+ synchronized(this) {
|
|
|
+ Reference<V> ref = map.get(key);
|
|
|
+ if(ref == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ // From the SoftReference we get the value, which can be
|
|
|
+ // null if it was removed in the processQueue() method defined below
|
|
|
+ V result = ref.get();
|
|
|
+ if (result == null) {
|
|
|
+ map.remove(key);
|
|
|
+ } else {
|
|
|
+ // hold value for a while...
|
|
|
+ list.hold(result);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Wstawia wartość do cache'u i zwraca ją jako wynik. Jeśli w mapie
|
|
|
+ * istniała już jakaś wartość, to ją zastępuje.
|
|
|
+ * @param key
|
|
|
+ * @param value
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public V pass(K key, V value) {
|
|
|
+ synchronized(this) {
|
|
|
+ release();
|
|
|
+ map.put(key, new ReferenceEntry<>(key, value, queue));
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public V put(K key, V value) {
|
|
|
+ synchronized(this) {
|
|
|
+ release();
|
|
|
+ V prev = asValue(map.get(key));
|
|
|
+ map.put(key, new ReferenceEntry<>(key, value, queue));
|
|
|
+ return prev;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public V remove(Object key) {
|
|
|
+ synchronized(this) {
|
|
|
+ release();
|
|
|
+ return asValue(map.remove(key));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void clear() {
|
|
|
+ synchronized(this) {
|
|
|
+ list.clear();
|
|
|
+ release();
|
|
|
+ map.clear();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int size() {
|
|
|
+ synchronized(this) {
|
|
|
+ release();
|
|
|
+ return map.size();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns number of holded entries
|
|
|
+ * Please note, that holdlist could contain duplicated entries,
|
|
|
+ * so returned number could be deceptively greater than size of map.
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public int holdsize() {
|
|
|
+ synchronized(this) {
|
|
|
+ release();
|
|
|
+ return list.size();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Holds value specified by key
|
|
|
+ * @param key
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public boolean hold(K key) {
|
|
|
+ synchronized(this) {
|
|
|
+ return null != get(key);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Holds as many entries as possible
|
|
|
+ * @return number of holded values
|
|
|
+ */
|
|
|
+ public int hold() {
|
|
|
+ synchronized(this) {
|
|
|
+ for(V value : values() ) {
|
|
|
+ if(null != value) {
|
|
|
+ list.hold(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return list.size();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Set<Entry<K,V>> entrySet() {
|
|
|
+ release();
|
|
|
+ return view.entrySet();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Set<K> keySet() {
|
|
|
+ release();
|
|
|
+ return view.keySet();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Collection<V> values() {
|
|
|
+ release();
|
|
|
+ return view.values();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void release() {
|
|
|
+ synchronized(this) {
|
|
|
+ Reference<? extends V> ref;
|
|
|
+ while ( null != (ref = queue.poll()) ) { map.remove( asKey(ref) ); }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static <TV> TV asValue(Reference<? extends TV> ref) {
|
|
|
+ return (ref != null) ? ref.get() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private static <TK> TK asKey(Reference<?> ref) {
|
|
|
+ return (ref != null) ? ((ReferenceEntry<TK,?>)ref).key : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ReferenceEntry<KT,VT> extends SoftReference<VT> {
|
|
|
+
|
|
|
+ private final KT key;
|
|
|
+
|
|
|
+ public ReferenceEntry(KT key, VT value, ReferenceQueue<VT> queue) {
|
|
|
+ super(value, queue);
|
|
|
+ this.key = key;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class CacheMap<TK,TV> extends LinkedHashMap<TK, Reference<TV>> {
|
|
|
+
|
|
|
+ private final int maxsize;
|
|
|
+
|
|
|
+ public CacheMap(int maxsize) {
|
|
|
+ super( Math.min(Math.round(maxsize / 0.75f)+2, 2048), 0.75f, true);
|
|
|
+ this.maxsize = maxsize;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected boolean removeEldestEntry(Entry<TK, Reference<TV>> eldest) {
|
|
|
+ return size() > maxsize;
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ private static class CacheList<TV> extends ArrayDeque<TV> {
|
|
|
+
|
|
|
+ private final int maxsize;
|
|
|
+
|
|
|
+ public CacheList(int maxsize) {
|
|
|
+ this.maxsize = maxsize;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void hold(TV value) {
|
|
|
+ addFirst(value);
|
|
|
+ if (size() > maxsize) { removeLast(); }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|