/* * Copyright 2013 Guidewire Software, Inc. */ package gw.util.concurrent; import java.io.Serializable; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.math.BigDecimal; import gw.util.concurrent.Cache.ConcurrentLinkedHashMap.Node.State; import gw.util.ILogger; /** * static var MY_CACHE = new Cache<Foo, Bar>( 1000, \ foo -> getBar( foo ) ) */ public class Cache<K, V> { private ConcurrentLinkedHashMap<K, V> _cacheImlp; private final MissHandler<K,V> _missHandler; private final String _name; private final int _size; //statistics private final AtomicInteger _requests = new AtomicInteger(); private final AtomicInteger _misses = new AtomicInteger(); private final AtomicInteger _hits = new AtomicInteger(); private ScheduledFuture<?> _loggingTask; /** This will create a new cache * * @param name the name of the cache for logging * @param size the maximum size of the log * @param missHandler how to handle misses, this is required not to be null */ public Cache( String name, int size, MissHandler<K, V> missHandler) { _name = name; _size = size; clearCacheImpl(); _missHandler = missHandler; } private void clearCacheImpl() { _cacheImlp = new ConcurrentLinkedHashMap<K,V>(ConcurrentLinkedHashMap.EvictionPolicy.SECOND_CHANCE, _size); } /** This will evict a specific key from the cache. * * @param key the key to evict * @return the current value for that key */ public V evict(K key) { return _cacheImlp.remove(key); } /** This will put a specific entry in the cache * * @param key this is the key * @param value this is the value * @return the old value for this key */ public V put(K key, V value) { return _cacheImlp.put(key, value); } /** This will get a specific entry, it will call the missHandler if it is not found. * * @param key the object to find * @return the found object (may be null) */ public V get(K key) { V value = _cacheImlp.get(key); _requests.incrementAndGet(); if (value == null) { value = _missHandler.load(key); _cacheImlp.put(key, value); _misses.incrementAndGet(); } else { _hits.incrementAndGet(); } return value; } public int getConfiguredSize() { return _size; } public int getUtilizedSize() { return _cacheImlp.size(); } public int getRequests() { return _requests.get(); } public int getMisses() { return _misses.get(); } public int getHits() { return _hits.get(); } public double getHitRate() { int requests = getRequests(); int hits = getHits(); if (requests == 0) { return 0.0; } else { return ((double) hits) / requests; } } /** * Sets up a recurring task every n seconds to report on the status of this cache. This can be useful * if you are doing exploratory caching and wish to monitor the performance of this cache with minimal fuss. * Consider * @param seconds how often to log the entry * @param logger the logger to use * @return this */ public synchronized Cache<K, V> logEveryNSeconds(int seconds, final ILogger logger) { if (_loggingTask == null) { ScheduledExecutorService service = Executors.newScheduledThreadPool(1); _loggingTask = service.scheduleAtFixedRate(new Runnable() { public void run() { logger.info(Cache.this); } }, seconds, seconds, TimeUnit.SECONDS); } else { throw new IllegalStateException("Logging for " + this + " is already enabled"); } return this; } public synchronized void stopLogging() { if (_loggingTask != null) { _loggingTask.cancel(false); } } public interface MissHandler<L, W> { public W load(L key); } public void clear() { clearCacheImpl(); _hits.set(0); _misses.set(0); _requests.set(0); } @Override public String toString() { return "Cache \"" + _name + "\"( Hits:" + getHits() + ", Misses:" + getMisses() + ", Requests:" + getRequests() + ", Hit rate:" + BigDecimal.valueOf(getHitRate() * 100.0).setScale(2, BigDecimal.ROUND_DOWN) + "% )"; } public static <K, V> Cache<K, V> make(String name, int size, MissHandler<K, V> handler) { return new Cache<K, V>(name, size, handler); } /** * A {@link ConcurrentMap} with a doubly-linked list running through its entries. * <p/> * This class provides the same semantics as a {@link ConcurrentHashMap} in terms of * iterators, acceptable keys, and concurrency characteristics, but perform slightly * worse due to the added expense of maintaining the linked list. It differs from * {@link java.util.LinkedHashMap} in that it does not provide predictable iteration * order. * <p/> * This map is intended to be used for caches and provides the following eviction policies: * <ul> * <li> First-in, First-out: Also known as insertion order. This policy has excellent * concurrency characteristics and an adequate hit rate. * <li> Second-chance: An enhanced FIFO policy that marks entries that have been retrieved * and saves them from being evicted until the next pass. This enhances the FIFO policy * by making it aware of "hot" entries, which increases its hit rate to be equal to an * LRU's under normal workloads. In the worst case, where all entries have been saved, * this policy degrades to a FIFO. * <li> Least Recently Used: An eviction policy based on the observation that entries that * have been used recently will likely be used again soon. This policy provides a good * approximation of an optimal algorithm, but suffers by being expensive to maintain. * The cost of reordering entries on the list during every access operation reduces * the concurrency and performance characteristics of this policy. * </ul> * <p/> * The <i>Second Chance</i> eviction policy is recommended for common use cases as it provides * the best mix of performance and efficiency of the supported replacement policies. * <p/> * If the <i>Least Recently Used</i> policy is chosen then the sizing should compensate for the * proliferation of dead nodes on the linked list. While the values are removed immediately, the * nodes are evicted only when they reach the head of the list. Under FIFO-based policies, dead * nodes occur when explicit removals are requested and does not normally produce a noticeable * impact on the map's hit rate. The LRU policy creates a dead node on every successful retrieval * and a new node is placed at the tail of the list. For this reason, the LRU's efficiency cannot * be compared directly to a {@link java.util.LinkedHashMap} evicting in access order. * * <a href="mailto:ben.manes@reardencommerce.com">Ben Manes</a> */ static class ConcurrentLinkedHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { private static final long serialVersionUID = 8350170357874293408L; final List<EvictionListener<K, V>> listeners; final ConcurrentMap<K, Node<K, V>> data; final AtomicInteger capacity; final EvictionPolicy policy; final AtomicInteger length; final Node<K, V> head; final Node<K, V> tail; /** * Creates a new, empty, unbounded map with the specified maximum capacity and the default * concurrencyLevel. * * @param policy The eviction policy to apply when the size exceeds the maximum capacity. * @param maximumCapacity The maximum capacity to coerces to. The size may exceed it temporarily. * @param listeners The listeners registered for notification when an entry is evicted. */ public ConcurrentLinkedHashMap(EvictionPolicy policy, int maximumCapacity, EvictionListener<K, V>... listeners) { this(policy, maximumCapacity, 16, listeners); } /** * Creates a new, empty, unbounded map with the specified maximum capacity and concurrency level. * * @param policy The eviction policy to apply when the size exceeds the maximum capacity. * @param maximumCapacity The maximum capacity to coerces to. The size may exceed it temporarily. * @param concurrencyLevel The estimated number of concurrently updating threads. The implementation * performs internal sizing to try to accommodate this many threads. * @param listeners The listeners registered for notification when an entry is evicted. */ public ConcurrentLinkedHashMap(EvictionPolicy policy, int maximumCapacity, int concurrencyLevel, EvictionListener<K, V>... listeners) { if ((policy == null) || (maximumCapacity < 0) || (concurrencyLevel <= 0)) { throw new IllegalArgumentException(); } this.listeners = (listeners == null) ? Collections.<EvictionListener<K, V>>emptyList() : Arrays.asList(listeners); this.data = new ConcurrentHashMap<K, Node<K, V>>(maximumCapacity, 0.75f, concurrencyLevel); this.capacity = new AtomicInteger(maximumCapacity); this.length = new AtomicInteger(); this.head = new Node<K, V>(); this.tail = new Node<K, V>(); this.policy = policy; head.setPrev(head); head.setNext(tail); tail.setPrev(head); tail.setNext(tail); } /** * Determines whether the map has exceeded its capacity. * * @return Whether the map has overflowed and an entry should be evicted. */ private boolean isOverflow() { return length.get() > capacity(); } /** * Sets the maximum capacity of the map and eagerly evicts entries until the * it shrinks to the appropriate size. * * @param capacity The maximum capacity of the map. */ public void setCapacity(int capacity) { if (capacity < 0) { throw new IllegalArgumentException(); } this.capacity.set(capacity); while (isOverflow()) { evict(); } } /** * Retrieves the maximum capacity of the map. * * @return The maximum capacity. */ public int capacity() { return capacity.get(); } /** * {@inheritDoc} */ @Override public int size() { return data.size(); } /** * {@inheritDoc} */ @Override public void clear() { for (K key : keySet()) { remove(key); } } /** * {@inheritDoc} */ @Override public boolean containsKey(Object key) { return data.containsKey(key); } /** * {@inheritDoc} */ @Override public boolean containsValue(Object value) { return data.containsValue(new Node<Object, Object>(null, value)); } /** * Evicts a single entry if the map exceeds the maximum capacity. */ private void evict() { while (isOverflow()) { Node<K, V> node = poll(); if (node == null) { return; } else if (policy.onEvict(this, node)) { V value = node.getValue(); if (value != null) { K key = node.getKey(); data.remove(key); notifyEviction(key, value); } length.decrementAndGet(); return; } offer(node); } } /** * Notifies the listeners that an entry was evicted from the map. * * @param key The entry's key. * @param value The entry's value. */ private void notifyEviction(K key, V value) { for (int i = 0; i < listeners.size(); i++) { EvictionListener<K, V> listener = listeners.get(i); listener.onEviction(key, value); } } /** * Retrieves and removes the first node on the list or <tt>null</tt> if empty. * * @return The first node on the list or <tt>null</tt> if empty. */ private Node<K, V> poll() { for (; ;) { Node<K, V> node = head.getNext(); if (head.casNext(node, node.getNext())) { for (; ;) { if (node.casState(State.LINKED, State.UNLINKING)) { node.getNext().setPrev(head); node.setState(State.UNLINKED); return node; } State state = node.getState(); if (state == State.SENTINEL) { return null; } } } } } /** * Inserts the specified node on to the tail of the list. * * @param node An unlinked node to append to the tail of the list. */ private void offer(Node<K, V> node) { node.setState(State.LINKING); node.setNext(tail); for (; ;) { Node<K, V> prev = tail.getPrev(); node.setPrev(prev); if (prev.casNext(tail, node)) { Node<K, V> next = tail; for (; ;) { if (next.casPrev(prev, node)) { node.setState(State.LINKED); return; } // walk up the list until a node can be linked next = next.getPrev(); } } } } /** * Adds a node to the list and data store if it does not already exist. * * @param node An unlinked node to add. * @return The previous value in the data store. */ private Node<K, V> putIfAbsent(Node<K, V> node) { Node<K, V> old = data.putIfAbsent(node.getKey(), node); if (old == null) { length.incrementAndGet(); offer(node); evict(); } return old; } /** * {@inheritDoc} */ @Override public V get(Object key) { Node<K, V> node = data.get(key); if (node != null) { V value = node.getValue(); policy.onGet(this, node); return value; } return null; } /** * {@inheritDoc} */ public V put(K key, V value) { if (value == null) { throw new IllegalArgumentException(); } Node<K, V> old = putIfAbsent(new Node<K, V>(key, value)); return (old == null) ? null : old.getAndSetValue(value); } /** * {@inheritDoc} */ public V putIfAbsent(K key, V value) { if (value == null) { throw new IllegalArgumentException(); } Node<K, V> old = putIfAbsent(new Node<K, V>(key, value)); return (old == null) ? null : old.getValue(); } /** * {@inheritDoc} */ public V remove(Object key) { Node<K, V> node = data.remove(key); if (node != null) { V value = node.getValue(); policy.onRemove(this, node); return value; } return null; } /** * {@inheritDoc} */ public boolean remove(Object key, Object value) { Node<K, V> node = data.get(key); if ((node != null) && node.value.equals(value) && data.remove(key, node)) { policy.onRemove(this, node); return true; } return false; } /** * {@inheritDoc} */ public V replace(K key, V value) { if (value == null) { throw new IllegalArgumentException(); } Node<K, V> node = data.get(key); return (node == null) ? null : node.getAndSetValue(value); } /** * {@inheritDoc} */ public boolean replace(K key, V oldValue, V newValue) { if (newValue == null) { throw new IllegalArgumentException(); } Node<K, V> node = data.get(key); return (node != null) && node.casValue(oldValue, newValue); } /** * {@inheritDoc} */ public Set<Entry<K, V>> entrySet() { return new EntrySetAdapter(); } /** * A listener registered for notification when an entry is evicted. */ public interface EvictionListener<K, V> { /** * A call-back notification that the entry was evicted. * * @param key The evicted key. * @param value The evicted value. */ void onEviction(K key, V value); } /** * The replacement policy to apply to determine which entry to discard to when the capacity has been reached. */ public enum EvictionPolicy { /** * Evicts entries based on insertion order. */ FIFO() { <K, V> void onGet(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { // do nothing } <K, V> boolean onEvict(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { return true; } }, /** * Evicts entries based on insertion order, but gives an entry a "second chance" if it has been requested recently. */ SECOND_CHANCE() { <K, V> void onGet(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { node.setMarked(true); } <K, V> void onRemove(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { super.onRemove(map, node); node.setMarked(false); } <K, V> boolean onEvict(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { if (node.isMarked()) { node.setMarked(false); return false; } return true; } }, /** * Evicts entries based on how recently they are used, with the least recent evicted first. */ LRU() { <K, V> void onGet(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { Node<K, V> newNode = new Node<K, V>(node.getKey(), node.getValue()); if (map.data.replace(node.getKey(), node, newNode)) { map.length.incrementAndGet(); onRemove(map, node); map.offer(newNode); map.evict(); } } <K, V> boolean onEvict(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { return true; } }; /** * Performs any operations required by the policy after a node was successfully retrieved. * @param map the map to for this listener * @param node the specific node */ abstract <K, V> void onGet(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node); /** * Expires a node so that, for all intents and purposes, it is a dead on the list. The * caller of this method should have already removed the node from the mapping so that * no key can look it up. When the node reaches the head of the list it will be evicted. * @param map the map to for this listener * @param node the specific node */ <K, V> void onRemove(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node) { node.setValue(null); } /** * Determines whether to evict the node at the head of the list. If false, the node is offered * to the tail. * @param map the map to for this listener * @param node the specific node * @return whether this node is to be evicted */ abstract <K, V> boolean onEvict(ConcurrentLinkedHashMap<K, V> map, Node<K, V> node); } /** * A node on the double-linked list. This list cross-cuts the data store. */ @SuppressWarnings("unchecked") static final class Node<K, V> implements Serializable { private static final long serialVersionUID = 1461281468985304519L; private static final AtomicReferenceFieldUpdater<Node, Object> valueUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Object.class, "value"); private static final AtomicReferenceFieldUpdater<Node, State> stateUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, State.class, "state"); private static final AtomicReferenceFieldUpdater<Node, Node> prevUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "prev"); private static final AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); public static enum State { SENTINEL, UNLINKED, UNLINKING, LINKING, LINKED } private final K key; private volatile V value; private volatile State state; private volatile boolean marked; private volatile Node<K, V> prev = null; private volatile Node<K, V> next = null; /** * Creates a sentinel node. */ public Node() { this.key = null; this.state = State.SENTINEL; } /** * Creates a new, unlinked node. * @param key the key * @param value the value */ public Node(K key, V value) { this.key = key; this.value = value; this.state = State.UNLINKED; } public K getKey() { return key; } public V getValue() { return value; } public void setValue(V value) { valueUpdater.set(this, value); } public V getAndSetValue(V value) { return (V) valueUpdater.getAndSet(this, value); } public boolean casValue(V expect, V update) { return valueUpdater.compareAndSet(this, expect, update); } public Node<K, V> getPrev() { return prev; } public void setPrev(Node<K, V> node) { prevUpdater.set(this, node); } public boolean casPrev(Node<K, V> expect, Node<K, V> update) { return prevUpdater.compareAndSet(this, expect, update); } public Node<K, V> getNext() { return next; } public void setNext(Node<K, V> node) { nextUpdater.set(this, node); } public boolean casNext(Node<K, V> expect, Node<K, V> update) { return nextUpdater.compareAndSet(this, expect, update); } public boolean isMarked() { return marked; } public void setMarked(boolean marked) { this.marked = marked; } public State getState() { return state; } public void setState(State state) { stateUpdater.set(this, state); } public boolean casState(State expect, State update) { return stateUpdater.compareAndSet(this, expect, update); } /** * Only ensures that the values are equal, as the key may be <tt>null</tt> for look-ups. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof Node)) { return false; } V value = getValue(); Node<?, ?> node = (Node<?, ?>) obj; return (value == null) ? (node.getValue() == null) : value.equals(node.getValue()); } /** * {@inheritDoc} */ @Override public int hashCode() { return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode()); } @Override public String toString() { return String.format("Node[state=%s, marked=%b, key=%s, value=%s]", getState(), isMarked(), getKey(), getValue()); } } /** * An adapter to represent the data store's entry set in the external type. */ private final class EntrySetAdapter extends AbstractSet<Entry<K, V>> { private final ConcurrentLinkedHashMap<K, V> map = ConcurrentLinkedHashMap.this; /** * {@inheritDoc} */ @Override public void clear() { map.clear(); } /** * {@inheritDoc} */ public int size() { return map.size(); } /** * {@inheritDoc} */ public Iterator<Entry<K, V>> iterator() { return new EntryIteratorAdapter(map.data.entrySet().iterator()); } /** * {@inheritDoc} */ @Override public boolean contains(Object obj) { if (!(obj instanceof Entry)) { return false; } Entry<?, ?> entry = (Entry<?, ?>) obj; Node<K, V> node = map.data.get(entry.getKey()); return (node != null) && (node.value.equals(entry.getValue())); } /** * {@inheritDoc} */ @Override public boolean add(Entry<K, V> entry) { return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); } /** * {@inheritDoc} */ @Override public boolean remove(Object obj) { if (!(obj instanceof Entry)) { return false; } Entry<?, ?> entry = (Entry<?, ?>) obj; return map.remove(entry.getKey(), entry.getValue()); } } /** * An adapter to represent the data store's entry iterator in the external type. */ private final class EntryIteratorAdapter implements Iterator<Entry<K, V>> { private final Iterator<Entry<K, Node<K, V>>> iterator; private Entry<K, Node<K, V>> current; public EntryIteratorAdapter(Iterator<Entry<K, Node<K, V>>> iterator) { this.iterator = iterator; } /** * {@inheritDoc} */ public boolean hasNext() { return iterator.hasNext(); } /** * {@inheritDoc} */ public Entry<K, V> next() { current = iterator.next(); K key = current.getKey(); Node<K, V> node = current.getValue(); V value = node == null ? null : node.getValue(); return new SimpleEntry<K, V>( key, value ); } /** * {@inheritDoc} */ public void remove() { if (current == null) { throw new IllegalStateException(); } ConcurrentLinkedHashMap.this.remove(current.getKey(), current.getValue()); current = null; } } /** * This duplicates {@link java.util.AbstractMap.SimpleEntry} until the class is made accessible. * Update: SimpleEntry is public in JDK 6. */ private static final class SimpleEntry<K, V> implements Entry<K, V> { private final K key; private V value; public SimpleEntry(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof Entry)) { return false; } Entry<?, ?> entry = (Entry<?, ?>) obj; return eq(key, entry.getKey()) && eq(value, entry.getValue()); } public int hashCode() { return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode()); } public String toString() { return key + "=" + value; } private static boolean eq(Object o1, Object o2) { return (o1 == null) ? (o2 == null) : o1.equals(o2); } } } }