manifold.util.concurrent.Cache Maven / Gradle / Ivy
/*
* Copyright (c) 2018 - Manifold Systems LLC
*
* 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 manifold.util.concurrent;
import java.io.Serializable;
import java.math.BigDecimal;
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.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import manifold.util.ILogger;
/**
* static var MY_CACHE = new Cache( 1000, \ foo -> getBar( foo ) )
*/
public class Cache
{
private ConcurrentLinkedHashMap _cacheImlp;
private final Loader _loader;
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 loader loads values into the cache, this is required not to be null
*/
public Cache( String name, int size, Loader loader )
{
_name = name;
_size = size;
clearCacheImpl();
_loader = loader;
}
private void clearCacheImpl()
{
_cacheImlp = new ConcurrentLinkedHashMap( 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 = _loader.load( key );
_cacheImlp.put( key, value );
_misses.incrementAndGet();
}
else
{
_hits.incrementAndGet();
}
return value;
}
public Iterable getCachedValues()
{
return _cacheImlp.values();
}
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 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 Loader
{
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 Cache make( String name, int size, Loader handler )
{
return new Cache( name, size, handler );
}
/**
* A {@link ConcurrentMap} with a doubly-linked list running through its entries.
*
* 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.
*
* This map is intended to be used for caches and provides the following eviction policies:
*
* - First-in, First-out: Also known as insertion order. This policy has excellent
* concurrency characteristics and an adequate hit rate.
*
- 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.
*
- 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.
*
*
* The Second Chance eviction policy is recommended for common use cases as it provides
* the best mix of performance and efficiency of the supported replacement policies.
*
* If the Least Recently Used 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.
*
* Ben Manes
*/
static class ConcurrentLinkedHashMap extends AbstractMap implements ConcurrentMap, Serializable
{
private static final long serialVersionUID = 8350170357874293408L;
final List> listeners;
final ConcurrentMap> data;
final AtomicInteger capacity;
final EvictionPolicy policy;
final AtomicInteger length;
final Node head;
final Node 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... 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... listeners )
{
if( (policy == null) || (maximumCapacity < 0) || (concurrencyLevel <= 0) )
{
throw new IllegalArgumentException();
}
this.listeners = (listeners == null) ? Collections.>emptyList() : Arrays.asList( listeners );
this.data = new ConcurrentHashMap>( maximumCapacity, 0.75f, concurrencyLevel );
this.capacity = new AtomicInteger( maximumCapacity );
this.length = new AtomicInteger();
this.head = new Node();
this.tail = new Node();
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