/*
* Copyright 2009 Benjamin Manes
*
* 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
*
* http://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 com.googlecode.concurrentlinkedhashmap.caches;
import java.io.Serializable;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 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>
*
* @author <a href="mailto:ben.manes@reardencommerce.com">Ben Manes</a>
*/
@SuppressWarnings("rawtypes")
public final class ProductionMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
private static final EvictionListener<?, ?> nullListener =
new EvictionListener<Object, Object>() {
@Override public void onEviction(Object key, Object value) {
}
};
private static final long serialVersionUID = 8350170357874293408L;
final ConcurrentMap<K, Node<K, V>> data;
final EvictionListener<K, V> listener;
final AtomicInteger capacity;
final EvictionPolicy policy;
final AtomicInteger length;
final Node<K, V> sentinel;
final Lock lock;
@SuppressWarnings("unchecked")
public ProductionMap(EvictionPolicy policy, CacheFactory builder) {
this.data = new ConcurrentHashMap<K, Node<K, V>>(
builder.initialCapacity, 0.75f, builder.concurrencyLevel);
this.capacity = new AtomicInteger(builder.maximumCapacity);
this.listener = (EvictionListener<K, V>) nullListener;
this.length = new AtomicInteger();
this.lock = new ReentrantLock();
this.sentinel = new Node<K, V>(lock);
this.policy = policy;
}
/**
* Determines whether the map has exceeded its capacity.
*
* @return Whether the map has overflowed and an entry should be evicted.
*/
private boolean isOverflow() {
return size() > capacity();
}
/**
* Sets the maximum capacity of the map and eagerly evicts entries until 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 (evict()) {
}
}
/**
* Retrieves the maximum capacity of the map.
*
* @return The maximum capacity.
*/
public int capacity() {
return capacity.get();
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
int size = length.get();
return (size >= 0) ? size : 0;
}
/**
* {@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) {
if (value == null) {
throw new IllegalArgumentException();
}
return data.containsValue(new Node<Object, Object>(null, value, null, lock));
}
/**
* Evicts a single entry if the map exceeds the maximum capacity.
*/
private boolean evict() {
while (isOverflow()) {
Node<K, V> node = sentinel.getNext();
if (node == sentinel) {
return false;
} else if (policy.onEvict(this, node)) {
// Attempt to remove the node if it's still available
if (data.remove(node.getKey(), new Identity(node))) {
length.decrementAndGet();
node.remove();
listener.onEviction(node.getKey(), node.getValue());
return true;
}
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public V get(Object key) {
Node<K, V> node = data.get(key);
if (node != null) {
policy.onAccess(this, node);
return node.getValue();
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public V put(K key, V value) {
if (value == null) {
throw new IllegalArgumentException();
}
Node<K, V> old = putIfAbsent(new Node<K, V>(key, value, sentinel, lock));
return (old == null) ? null : old.getAndSetValue(value);
}
/**
* {@inheritDoc}
*/
@Override
public V putIfAbsent(K key, V value) {
if (value == null) {
throw new IllegalArgumentException();
}
Node<K, V> old = putIfAbsent(new Node<K, V>(key, value, sentinel, lock));
return (old == null) ? null : old.getValue();
}
/**
* 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();
node.appendToTail();
evict();
} else {
policy.onAccess(this, old);
}
return old;
}
/**
* {@inheritDoc}
*/
@Override
public V remove(Object key) {
Node<K, V> node = data.remove(key);
if (node == null) {
return null;
}
length.decrementAndGet();
node.remove();
return node.getValue();
}
/**
* {@inheritDoc}
*/
@Override
public boolean remove(Object key, Object value) {
Node<K, V> node = data.get(key);
if ((node != null) && node.value.equals(value) && data.remove(key, new Identity(node))) {
length.decrementAndGet();
node.remove();
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
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}
*/
@Override
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) ? false : node.casValue(oldValue, newValue);
}
/**
* {@inheritDoc}
*/
@Override
public Set<K> keySet() {
return new KeySet();
}
/**
* {@inheritDoc}
*/
@Override
public Collection<V> values() {
return new Values();
}
/**
* {@inheritDoc}
*/
@Override
public Set<Entry<K, V>> entrySet() {
return new EntrySet();
}
/**
* 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 when
* the capacity has been reached.
*/
public enum EvictionPolicy {
/**
* Evicts entries based on insertion order.
*/
FIFO() {
@Override
<K, V> void onAccess(ProductionMap<K, V> map, Node<K, V> node) {
// do nothing
}
@Override
<K, V> boolean onEvict(ProductionMap<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() {
@Override
<K, V> void onAccess(ProductionMap<K, V> map, Node<K, V> node) {
node.setMarked(true);
}
@Override
<K, V> boolean onEvict(ProductionMap<K, V> map, Node<K, V> node) {
if (node.isMarked()) {
node.moveToTail();
node.setMarked(false);
return false;
}
return true;
}
},
/**
* Evicts entries based on how recently they are used, with the least recent
* evicted first.
*/
LRU() {
@Override
<K, V> void onAccess(ProductionMap<K, V> map, Node<K, V> node) {
node.moveToTail();
}
@Override
<K, V> boolean onEvict(ProductionMap<K, V> map, Node<K, V> node) {
return true;
}
};
/**
* Performs any operations required by the policy after a node was
* successfully retrieved.
*/
abstract <K, V> void onAccess(ProductionMap<K, V> map, Node<K, V> node);
/**
* Determines whether to evict the node at the head of the list.
*/
abstract <K, V> boolean onEvict(ProductionMap<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 Node UNLINKED = new Node(null);
private final K key;
private final Lock lock;
private final Node<K, V> sentinel;
private volatile V value;
private volatile boolean marked;
private volatile Node<K, V> prev;
private volatile Node<K, V> next;
/**
* Creates a new sentinel node.
*/
public Node(Lock lock) {
this.sentinel = this;
this.value = null;
this.lock = lock;
this.prev = this;
this.next = this;
this.key = null;
}
/**
* Creates a new, unlinked node.
*/
public Node(K key, V value, Node<K, V> sentinel, Lock lock) {
this.sentinel = sentinel;
this.next = UNLINKED;
this.prev = UNLINKED;
this.value = value;
this.lock = lock;
this.key = key;
}
/**
* Appends the node to the tail of the list.
*/
public void appendToTail() {
lock.lock();
try {
// Allow moveToTail() to no-op or removal to spin-wait
next = sentinel;
// Read the tail on the stack to avoid unnecessary volatile reads
final Node<K, V> tail = sentinel.prev;
sentinel.prev = this;
tail.next = this;
prev = tail;
} finally {
lock.unlock();
}
}
/**
* Removes the node from the list.
* <p>
* If the node has not yet been appended to the tail it will wait for that
* operation to complete.
*/
public void remove() {
for (;;) {
if (isUnlinked()) {
continue; // await appendToTail()
}
lock.lock();
try {
if (isUnlinked()) {
continue; // await appendToTail()
}
prev.next = next;
next.prev = prev;
next = UNLINKED; // mark as unlinked
} finally {
lock.unlock();
}
return;
}
}
/**
* Moves the node to the tail.
* <p>
* If the node has been unlinked or is already at the tail, no-ops.
*/
public void moveToTail() {
if (isTail() || isUnlinked()) {
return;
}
lock.lock();
try {
if (isTail() || isUnlinked()) {
return;
}
// unlink
prev.next = next;
next.prev = prev;
// link
next = sentinel; // ordered for isTail()
prev = sentinel.prev;
sentinel.prev = this;
prev.next = this;
} finally {
lock.unlock();
}
}
/**
* Checks whether the node is linked on the list chain.
*
* @return Whether the node has not yet been linked on the list.
*/
public boolean isUnlinked() {
return (next == UNLINKED);
}
/**
* Checks whether the node is the last linked on the list chain.
*
* @return Whether the node is at the tail of the list.
*/
public boolean isTail() {
return (next == sentinel);
}
/*
* Key operators
*/
public K getKey() {
return key;
}
/*
* Value operators
*/
public V getValue() {
return (V) valueUpdater.get(this);
}
public V getAndSetValue(V value) {
return (V) valueUpdater.getAndSet(this, value);
}
public boolean casValue(V expect, V update) {
return valueUpdater.compareAndSet(this, expect, update);
}
/*
* Previous node operators
*/
public Node<K, V> getPrev() {
return prev;
}
/*
* Next node operators
*/
public Node<K, V> getNext() {
return next;
}
/*
* Access frequency operators
*/
public boolean isMarked() {
return marked;
}
public void setMarked(boolean marked) {
this.marked = marked;
}
/**
* 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());
}
@Override
public int hashCode() {
return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode());
}
}
/**
* Allows {@link #equals(Object)} to compare using object identity.
*/
private static final class Identity {
private final Object delegate;
public Identity(Object delegate) {
this.delegate = delegate;
}
@Override
public boolean equals(Object o) {
return (o == delegate);
}
}
/**
* An adapter to safely externalize the keys.
*/
private final class KeySet extends AbstractSet<K> {
private final ProductionMap<K, V> map = ProductionMap.this;
@Override
public int size() {
return map.size();
}
@Override
public void clear() {
map.clear();
}
@Override
public Iterator<K> iterator() {
return new KeyIterator();
}
@Override
public boolean contains(Object obj) {
return map.containsKey(obj);
}
@Override
public boolean remove(Object obj) {
return (map.remove(obj) != null);
}
@Override
public Object[] toArray() {
return map.data.keySet().toArray();
}
@Override
public <T> T[] toArray(T[] array) {
return map.data.keySet().toArray(array);
}
}
/**
* An adapter to safely externalize the keys.
*/
private final class KeyIterator implements Iterator<K> {
private final EntryIterator iterator =
new EntryIterator(ProductionMap.this.data.values().iterator());
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public K next() {
return iterator.next().getKey();
}
@Override
public void remove() {
iterator.remove();
}
}
/**
* An adapter to represent the data store's values in the external type.
*/
private final class Values extends AbstractCollection<V> {
private final ProductionMap<K, V> map = ProductionMap.this;
@Override
public int size() {
return map.size();
}
@Override
public void clear() {
map.clear();
}
@Override
public Iterator<V> iterator() {
return new ValueIterator();
}
@Override
public boolean contains(Object o) {
return map.containsValue(o);
}
@Override
public Object[] toArray() {
Collection<V> values = new ArrayList<V>(size());
for (V value : this) {
values.add(value);
}
return values.toArray();
}
@Override
public <T> T[] toArray(T[] array) {
Collection<V> values = new ArrayList<V>(size());
for (V value : this) {
values.add(value);
}
return values.toArray(array);
}
}
/**
* An adapter to represent the data store's values in the external type.
*/
private final class ValueIterator implements Iterator<V> {
private final EntryIterator iterator =
new EntryIterator(ProductionMap.this.data.values().iterator());
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public V next() {
return iterator.next().getValue();
}
@Override
public void remove() {
iterator.remove();
}
}
/**
* An adapter to represent the data store's entry set in the external type.
*/
private final class EntrySet extends AbstractSet<Entry<K, V>> {
private final ProductionMap<K, V> map = ProductionMap.this;
@Override
public int size() {
return map.size();
}
@Override
public void clear() {
map.clear();
}
@Override
public Iterator<Entry<K, V>> iterator() {
return new EntryIterator(map.data.values().iterator());
}
@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()));
}
@Override
public boolean add(Entry<K, V> entry) {
return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null);
}
@Override
public boolean remove(Object obj) {
if (!(obj instanceof Entry<?, ?>)) {
return false;
}
Entry<?, ?> entry = (Entry<?, ?>) obj;
return map.remove(entry.getKey(), entry.getValue());
}
@Override
public Object[] toArray() {
Collection<Entry<K, V>> entries = new ArrayList<Entry<K, V>>(size());
for (Entry<K, V> entry : this) {
entries.add(new SimpleEntry<K, V>(entry));
}
return entries.toArray();
}
@Override
public <T> T[] toArray(T[] array) {
Collection<Entry<K, V>> entries = new ArrayList<Entry<K, V>>(size());
for (Entry<K, V> entry : this) {
entries.add(new SimpleEntry<K, V>(entry));
}
return entries.toArray(array);
}
}
/**
* An adapter to represent the data store's entry iterator in the external
* type.
*/
private final class EntryIterator implements Iterator<Entry<K, V>> {
private final Iterator<Node<K, V>> iterator;
private Entry<K, V> current;
public EntryIterator(Iterator<Node<K, V>> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Entry<K, V> next() {
current = new NodeEntry(iterator.next());
return current;
}
@Override
public void remove() {
if (current == null) {
throw new IllegalStateException();
}
ProductionMap.this.remove(current.getKey(), current.getValue());
current = null;
}
}
/**
* An entry that is tied to the map instance to allow updates through the
* entry or the map to be visible.
*/
private final class NodeEntry implements Entry<K, V> {
private final ProductionMap<K, V> map = ProductionMap.this;
private final Node<K, V> node;
public NodeEntry(Node<K, V> node) {
this.node = node;
}
@Override
public K getKey() {
return node.getKey();
}
@Override
public V getValue() {
if (node.isUnlinked()) {
V value = map.get(getKey());
if (value != null) {
return value;
}
}
return node.getValue();
}
@Override
public V setValue(V value) {
return map.replace(getKey(), value);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Entry<?, ?>)) {
return false;
}
Entry<?, ?> entry = (Entry<?, ?>) obj;
return eq(getKey(), entry.getKey()) && eq(getValue(), entry.getValue());
}
@Override
public int hashCode() {
K key = getKey();
V value = getValue();
return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode());
}
@Override
public String toString() {
return getKey() + "=" + getValue();
}
private boolean eq(Object o1, Object o2) {
return (o1 == null) ? (o2 == null) : o1.equals(o2);
}
}
}