zcommon/src/main/java/org/zkoss/util/FastReadCache.java
/* 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;
}
}
}