FastCache.java Source code

Java tutorial

Introduction

Here is the source code for FastCache.java

Source

//package com.aliasi.util;

//import com.aliasi.util.AbstractExternalizable;

import java.lang.ref.SoftReference;

import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

import java.util.AbstractMap;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;

/**
 * A <code>FastCache</code> is a map implemented with soft references,
 * optimistic copy-on-write updates, and approximate count-based
 * pruning.  It is intended for scalable multi-threaded caches.  It
 * sacrifices recall of mappings for speed of put and get operations
 * along with conservative memory guarantees.
 *
 * <p><i>Note:</i>The class {@link HardFastCache} is nearly identical
 * to this class, but with no soft references around hash buckets.
 *
 * <p>The basis of the cache is a fixed-size hash map, based on values
 * returned by objects' <code>hashCode()</code> and
 * <code>equals(Object)</code> methods.
 *
 * <p>The map entries in the hash map are stored in buckets held by
 * soft references.  Thus entries in the mapping may be garbage
 * collected.  In the implementation, whole hash buckets are
 * collected, which is safe and highly efficient but may require some
 * additional recomputation of values that were removed by pruning.
 *
 * <p>Entries are stored in the map using a very optimistic update.
 * No synchronization at all is performed on the cache entries or
 * their counts.  A copy-on-write strategy coupled with Java's memory
 * model for references ensures that the cache remains consistent, if
 * not complete.  What this means is that multiple threads may both
 * try to cache a mapping and only one will be saved and/or
 * incremented in count.
 *
 * <p>When the table approximately exceeds the specified load factor,
 * the thread performing the add will perform a garbage collection by
 * reducing reference counts by half, rounding down, and removing
 * entries with zero counts.  Pruning is subject to the caveats
 * mentioned in the last paragraph.  Counts are not guaranteed to be
 * accurate.  Pruning itself is synchronized and more conservatively
 * copy-on-write.  By setting the load factor to
 * <code>Double.POSITIVE_INFINITY</code> there will be never be any
 * pruning done by this class; all pruning will take place by soft
 * reference garbage collection.
 *
 * <p>A fast cache acts as a mapping with copy-on-write semantics.
 * Equality and iteration are defined as usual, with the caveat that
 * the snapshot taken of the elements may not be up to date.  Iterators
 * may be used concurrently, but their remove methods do not affect
 * the underlying cache.
 *
 * <p><b>Serialization</b></p>
 *
 * <p>A fast cache may be serialized if the keys and values it
 * contains are serializable.  It may always be serialized if
 * it is first cleared using {@link #clear()}.
 *
 * <p><b>References</b></p>
 *
 * <p>For more information on soft references, see:
 *
 * <ul>
 * <li> Peter Hagar. 2002. <a href="http://www-128.ibm.com/developerworks/library/j-refs/">Guidelines for using the Java 2 reference classes</a>. IBM DeveloperWorks.
 * <li> Jeff Friesen. 2002. <a href="http://www.javaworld.com/javaworld/jw-01-2002/jw-0104-java101.html">Trash talk, part 2: reference objects API</a>. JavaWorld.
 * </ul>
 *
 * For information on copy-on-write and optimistic updates,
 * see section 2.4 of:
 *
 * <ul>
 * <li> Doug Lea.  2000.
 * <i>Concurrent Programming in Java. Second Edition.</i>
 * Addison-Wesley.
 * </ul>
 *
 * @author  Bob Carpenter
 * @version 3.8.3
 * @since   LingPipe2.2
 * @param <K> the type of keys in the map
 * @param <V> the type of values in the map
 */
public class FastCache<K, V> extends AbstractMap<K, V> implements Serializable {

    static final long serialVersionUID = 3003326726041067827L;

    private static final double DEFAULT_LOAD_FACTOR = 0.5;

    private final SoftReference<Record<K, V>>[] mBuckets;

    private volatile int mNumEntries = 0;

    private int mMaxEntries;

    /**
     * Constrcut a fast cache of the specified size and default load
     * factor.  The default load factor is 0.5.  See {@link
     * #FastCache(int,double)} for more information.
     *
     * @param size Number of buckets in cache
     * @throws IllegalArgumentException if the size is less than 2.
     */
    public FastCache(int size) {
        this(size, DEFAULT_LOAD_FACTOR);
    }

    FastCache(int maxEntries, int numBuckets, boolean ignoreMe) {
        mMaxEntries = maxEntries;
        // cut-and-paste from below, must be in-constructor for final
        @SuppressWarnings({ "unchecked", "rawtypes" })
        SoftReference<Record<K, V>>[] bucketsTemp = (SoftReference<Record<K, V>>[]) new SoftReference[numBuckets];
        mBuckets = bucketsTemp;
    }

    /**
     * Construct a fast cache of the specified size (measured in
     * number of hash buckets) and load factor.  The size times the
     * load factor must be greater than or equal to 1.  When the
     * (approximate) number of entries exceeds the load factor times
     * the size, the cache is pruned.
     *
     * @param size Number of buckets in the cache.
     * @param loadFactor Load factor of the cache.
     * @throws IllegalArgumentException If the size is less than one or the load
     * factor is not a positive finite value.
     */
    public FastCache(int size, double loadFactor) {
        if (size < 1) {
            String msg = "Cache size must be at least 1." + " Found cache size=" + size;
            throw new IllegalArgumentException(msg);
        }
        if (loadFactor < 0.0 || Double.isNaN(loadFactor) || Double.isInfinite(loadFactor)) {
            String msg = "Load factor must be finite and positive." + " found loadFactor=" + loadFactor;
            throw new IllegalArgumentException(msg);
        }
        mMaxEntries = (int) (loadFactor * (double) size);
        if (mMaxEntries < 1) {
            String msg = "size * loadFactor must be > 0." + " Found size=" + size + " loadFactor=" + loadFactor;
            throw new IllegalArgumentException(msg);
        }
        // required for array
        @SuppressWarnings({ "unchecked", "rawtypes" })
        SoftReference<Record<K, V>>[] bucketsTemp = (SoftReference<Record<K, V>>[]) new SoftReference[size];
        mBuckets = bucketsTemp;
    }

    Record<K, V> getFirstRecord(int bucketId) {
        SoftReference<Record<K, V>> ref = mBuckets[bucketId];
        return ref == null ? null : ref.get();
    }

    void setFirstRecord(int bucketId, Record<K, V> record) {
        SoftReference<Record<K, V>> ref = new SoftReference<Record<K, V>>(record);
        mBuckets[bucketId] = ref;
    }

    /**
     * Returns the value of the specified key or <code>null</code> if
     * there is no value attached.  Note that the argument is not
     * the generic <code>&lt;K&gt;</code> key type, but <code>Object</code>
     * to match the requirements of <code>java.util.Map</code>.
     *
     * <p><i>Warning:</i> Because of the approximate cache-like
     * behavior of this class, key-value pairs previously added
     * by the {@link #put(Object,Object)} method may disappear.
     *
     * @param key Mapping key.
     * @return The value for the specified key.
     */
    @Override
    public V get(Object key) {
        int bucketId = bucketId(key);
        for (Record<K, V> record = getFirstRecord(bucketId); record != null; record = record.mNextRecord) {

            if (record.mKey.equals(key)) {
                ++record.mCount;
                return record.mValue;
            }
        }
        return null;
    }

    int bucketId(Object key) {
        return java.lang.Math.abs(key.hashCode() % mBuckets.length);
    }

    /**
     * Sets the value of the specified key to the specified value.
     * If there is already a value for the specified key, the count
     * is incremented, but the value is not replaced.
     *
     * <p><i>Warning:</i> Because of the approximate cache-like
     * behavior of this class, setting the value of a key with this
     * method is not guaranteed to replace older values or remain in
     * the mapping until the next call to {@link #get(Object)}.
     *
     * @param key Mapping key.
     * @param value New value for the specified key.
     * @return <code>null</code>, even if there is an existing
     * value for the specified key.
     */
    @Override
    public V put(K key, V value) {
        int bucketId = bucketId(key);
        Record<K, V> firstRecord = getFirstRecord(bucketId);
        for (Record<K, V> record = firstRecord; record != null; record = record.mNextRecord) {
            if (record.mKey.equals(key)) {
                ++record.mCount; // increment instead
                return null; // already there
            }
        }
        prune();
        firstRecord = getFirstRecord(bucketId); // may've been pruned
        Record<K, V> record = new Record<K, V>(key, value, firstRecord);
        setFirstRecord(bucketId, record);
        ++mNumEntries;
        return null;
    }

    /**
     * Removes all entries from this cache.
     */
    public void clear() {
        synchronized (this) {
            for (SoftReference<Record<K, V>> ref : mBuckets)
                if (ref != null)
                    ref.clear();
        }
    }

    /**
     * Prunes this cache by (approximately) dividing the counts of
     * entries by two and removing the ones with zero counts.  This
     * operation is approximate in the sense that the optimistic
     * update strategy applied is not guaranteed to actually do any
     * pruning or decrements of counts.
     */
    public void prune() {
        // only synchronized versus other prunes;
        // other puts, etc. may interfere, which is OK
        synchronized (this) {
            if (mNumEntries < mMaxEntries)
                return;
            int count = 0;
            for (int i = 0; i < mBuckets.length; ++i) {
                Record<K, V> record = getFirstRecord(i);
                Record<K, V> prunedRecord = prune(record);
                setFirstRecord(i, prunedRecord);
                for (Record<K, V> r = prunedRecord; r != null; r = r.mNextRecord)
                    ++count;
            }
            mNumEntries = count;
        }
    }

    final Record<K, V> prune(Record<K, V> inRecord) {
        Record<K, V> record = inRecord;
        while (record != null && (record.mCount = (record.mCount >>> 1)) == 0)
            record = record.mNextRecord;
        if (record == null)
            return null;
        record.mNextRecord = prune(record.mNextRecord);
        return record;
    }

    /**
     * Returns a snapshot of the entries in this map.
     * This set is not backed by this cache, so that changes
     * to the cache do not affect the cache and vice-versa.
     *
     * @return The set of entries in this cache.
     */
    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        HashSet<Map.Entry<K, V>> entrySet = new HashSet<Map.Entry<K, V>>();
        for (int i = 0; i < mBuckets.length; ++i)
            for (Record<K, V> record = getFirstRecord(i); record != null; record = record.mNextRecord)
                entrySet.add(record);
        return entrySet;
    }

    static final class Record<K, V> implements Map.Entry<K, V> {
        final K mKey;
        final V mValue;
        volatile Record<K, V> mNextRecord;
        volatile int mCount;

        Record(K key, V value) {
            this(key, value, null);
        }

        Record(K key, V value, Record<K, V> nextRecord) {
            this(key, value, nextRecord, 1);
        }

        Record(K key, V value, Record<K, V> nextRecord, int count) {
            mKey = key;
            mValue = value;
            mNextRecord = nextRecord;
            mCount = count;
        }

        public K getKey() {
            return mKey;
        }

        public V getValue() {
            return mValue;
        }

        @Override
        public int hashCode() {
            return (mKey == null ? 0 : mKey.hashCode()) ^ (mValue == null ? 0 : mValue.hashCode());
        }

        @Override
        @SuppressWarnings("rawtypes") // for instanceof
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?, ?> e2 = (Map.Entry<?, ?>) o;
            return (mKey == null ? e2.getKey() == null : mKey.equals(e2.getKey()))
                    && (mValue == null ? e2.getValue() == null : mValue.equals(e2.getValue()));
        }

        public V setValue(V value) {
            String msg = "Cache records may not be set.";
            throw new UnsupportedOperationException(msg);
        }
    }
}