zcommon/src/main/java/org/zkoss/util/FastReadCache.java

Summary

Maintainability
A
1 hr
Test Coverage
/* FastReadCache.java

    History:
        Thu Jan 19 11:36:10 TST 2012, Created by tomyeh

Copyright (C) 2012 Potix Corporation. All Rights Reserved.
*/
package org.zkoss.util;

import java.util.Map;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;

import org.zkoss.lang.Objects;

/**
 * A {@link CacheMap} that the possibility to have cache hit is much more than
 * not. It maintains a readonly cache (so no need to synchronize), and then
 * clone and replace it if there is a miss.
 * Thus, as time goes, most access can go directly to the readonly cache
 * without any synchronization or cloning.
 * <p>Thread safe.
 * @author tomyeh
 * @since 6.0.0
 */
public class FastReadCache<K, V> implements Cache<K, V>, java.io.Serializable, Cloneable {
    private InnerCache _cache;
    private Map<K, V> _writeCache;
    private transient short _missCnt;
    private transient short _maxMissCnt = 100;
    /** whether _writeCache is different from _cache. */
    private boolean _moreInWriteCache;

    /** Constructor.
     */
    public FastReadCache() {
        this(DEFAULT_MAX_SIZE, DEFAULT_LIFETIME);
    }
    /** Constructor.
     */
    public FastReadCache(int maxSize, int lifetime) {
        _cache = new InnerCache(maxSize, lifetime);
    }

    /** Constructor.
     * @param maxMissCount a short value from 0 to this for sync the read cache,
     * default is 100.  
     * @since 6.5.2
     */
    public FastReadCache(int maxSize, int lifetime, short maxMissCount) {
        _cache = new InnerCache(maxSize, lifetime);
        _maxMissCnt = maxMissCount;
    }
        
    public boolean containsKey(Object key) {
        boolean found = _cache.containsKey(key);
        if (!found && _moreInWriteCache)
            synchronized (this) {
                if (_writeCache != null && (found = _writeCache.containsKey(key)))
                    missed();
            }
        return found;
    }
    
    public V get(Object key) {
        V val = _cache.get(key);
        if (val == null && _moreInWriteCache)
            synchronized (this) {
                if (_writeCache != null && (val = _writeCache.get(key)) != null)
                    missed();
            }
        return val;
    }
    
    public V put(K key, V value) {
        V result = value;
        synchronized (this) {
            if (!Objects.equals(value, _cache.getWithoutExpunge(key))) {
                result = syncToWriteCache().put(key, value);
                _moreInWriteCache = true;

                if (_cache.containsKeyWithoutExpunge(key)) //ensure _writeCache >= _cache
                    syncToReadCache();
            }
        }
        return result;
    }
    
    public V remove(Object key) {
        V result = null;
        synchronized (this) {
            if (!_cache.containsKeyWithoutExpunge(key) && !_moreInWriteCache)
                return null; //not found at all

            result = syncToWriteCache().remove(key);
            if (_cache.containsKeyWithoutExpunge(key)) //ensure _writeCache >= _cache
                syncToReadCache();
        }
        return result;
    }
    
    public void clear() {
        synchronized (this) {
            setReadAndClearWrite(new InnerCache(getMaxSize(), getLifetime()));
        }
    }

    //synchronized(this) before calling this
    private void missed() {
        //If missed too many times, we copy _writeCache back to _cache
        //
        //Note: we don't count it a miss if both _writeCache and _cache don't have
        //because it implies the same thread (i.e., only  a few thread,
        //so synchronized(this) overhead is small)
        if (++_missCnt == _maxMissCnt)
            syncToReadCache();
    }
    /** Synchronizes _writeCache to _cache.
     ** <p>synchronized(this) before calling this
     */
    private void syncToReadCache() {
        //don't check _moreInWriteCache here because it might be called
        //in remove() (when _writeCache is less but still need to sync)

        _missCnt = 0;
        _moreInWriteCache = false;

        final InnerCache cache = new InnerCache(getMaxSize(), getLifetime());
        cache.putAll(_writeCache);
        _cache = cache;
        //no need free to free _writeCache, since write faster (GC will clear it in expunge)
    }
    /** Synchronized from _cache to _writeCache.
     ** <p>synchronized(this) before calling this
     */
    private Map<K,V> syncToWriteCache() {
        if (_writeCache == null) {
            _writeCache = new LinkedHashMap<K, V>();
                //order is important because cache's expunge depends on it
            _writeCache.putAll(_cache);
        }
        return _writeCache;
    }
    //synchronized(this) before calling this
    private void setReadAndClearWrite(InnerCache cache) {
        _writeCache = null;
        _missCnt = 0;
        _moreInWriteCache = false;
        _cache = cache;
    }
    
    public int getLifetime() {
        return _cache.getLifetime();
    }
    
    public void setLifetime(int lifetime) {
        _cache.setLifetime(lifetime);
    }
    
    public int getMaxSize() {
        return _cache.getMaxSize();
    }
    
    public void setMaxSize(int maxsize) {
        _cache.setMaxSize(maxsize);
    }

    private class InnerCache extends CacheMap<K, V> {
        private List<K> _removed;

        private InnerCache() {
            super(false);
            //insertion-order since _cache be read concurrently
        }
        private InnerCache(int maxSize, int lifetime) {
            super(maxSize, lifetime, false);
            //insertion-order since _cache be read concurrently
        }
         /*package*/
        void removeInExpunge(Iterator<Map.Entry<K, Value<V>>> it, K key) {
            _removed.add(key);
                //don't remove it here since _cache (this) is readonly
        }
         /*package*/
        void doExpunge() {
            synchronized (FastReadCache.this) {
                _removed = new ArrayList<K>();
                try {
                    super.doExpunge();

                    if (!_removed.isEmpty()) {
                        final InnerCache cache = (InnerCache)this.clone();
                        for (final K key: _removed)
                            cache.remove(key);
                        setReadAndClearWrite(cache);
                    }
                } finally {
                    _removed = null;
                }
            }
        }
        
        public Object clone() {
            final InnerCache clone = (InnerCache)super.clone();
            clone._removed = null;
            return clone;
        }
    }
}