ALF-15888: change implementation of DefaultSimpleCache to use Google's ConcurrentLinkedHashMap.

After much deliberation I decided not to offer an unbounded cache size, i.e. maxItems MUST be at least one. This simplifies the implementation (marginally) and means that tests do not have to be duplicated for both underlying data structure types (better coverage). Would we really want a cache to grow indefinitely?

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@42223 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Matt Ward
2012-10-01 16:47:43 +00:00
parent 511af90d5c
commit 99b47ce435
2 changed files with 70 additions and 52 deletions

View File

@@ -19,39 +19,35 @@
package org.alfresco.repo.cache; package org.alfresco.repo.cache;
import java.io.Serializable; import java.io.Serializable;
import java.util.AbstractMap;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.googlecode.concurrentlinkedhashmap.Weighers;
/** /**
* {@link SimpleCache} implementation backed by a {@link LinkedHashMap}. * {@link SimpleCache} implementation backed by a {@link ConcurrentLinkedHashMap}.
* *
* @author Matt Ward * @author Matt Ward
*/ */
public final class DefaultSimpleCache<K extends Serializable, V extends Object> public final class DefaultSimpleCache<K extends Serializable, V extends Object>
implements SimpleCache<K, V>, BeanNameAware implements SimpleCache<K, V>, BeanNameAware, InitializingBean
{ {
private final Map<K, V> map; private Map<K, AbstractMap.SimpleImmutableEntry<K, V>> map;
private int maxItems = 0; private int maxItems = 1000000;
private String cacheName; private String cacheName;
/**
* Default constructor. {@link #afterPropertiesSet()} MUST be called before the cache
* may be used when the cache is constructed using the default constructor.
*/
public DefaultSimpleCache() public DefaultSimpleCache()
{ {
// Create a LinkedHashMap with accessOrder true, i.e. iteration order
// will be least recently accessed first. Eviction policy will therefore be LRU.
// The map will have a bounded size determined by the maxItems member variable.
map = (Map<K, V>) Collections.synchronizedMap(new LinkedHashMap<K, V>(16, 0.75f, true) {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest)
{
return maxItems > 0 && size() > maxItems;
}
});
} }
@Override @Override
@@ -69,13 +65,19 @@ public final class DefaultSimpleCache<K extends Serializable, V extends Object>
@Override @Override
public V get(K key) public V get(K key)
{ {
return map.get(key); AbstractMap.SimpleImmutableEntry<K, V> kvp = map.get(key);
if (kvp == null)
{
return null;
}
return kvp.getValue();
} }
@Override @Override
public void put(K key, V value) public void put(K key, V value)
{ {
map.put(key, value); AbstractMap.SimpleImmutableEntry<K, V> kvp = new AbstractMap.SimpleImmutableEntry<K, V>(key, value);
map.put(key, kvp);
} }
@Override @Override
@@ -97,19 +99,14 @@ public final class DefaultSimpleCache<K extends Serializable, V extends Object>
} }
/** /**
* Sets the maximum number of items that the cache will hold. Setting * Sets the maximum number of items that the cache will hold. The cache
* this value will cause the cache to be emptied. A value of zero * must be re-initialised if already in existence using {@link #afterPropertiesSet()}.
* will allow the cache to grow unbounded.
* *
* @param maxItems * @param maxItems
*/ */
public void setMaxItems(int maxItems) public synchronized void setMaxItems(int maxItems)
{ {
synchronized(map) this.maxItems = maxItems;
{
map.clear();
this.maxItems = maxItems;
}
} }
/** /**
@@ -123,4 +120,26 @@ public final class DefaultSimpleCache<K extends Serializable, V extends Object>
{ {
this.cacheName = cacheName; this.cacheName = cacheName;
} }
/**
* Initialise the cache.
*
* @throws Exception
*/
@Override
public synchronized void afterPropertiesSet() throws Exception
{
if (maxItems < 1)
{
throw new IllegalArgumentException("maxItems property must be a positive integer.");
}
// The map will have a bounded size determined by the maxItems member variable.
map = new ConcurrentLinkedHashMap.Builder<K, AbstractMap.SimpleImmutableEntry<K, V>>()
.maximumWeightedCapacity(maxItems)
.concurrencyLevel(32)
.weigher(Weighers.singleton())
.build();
}
} }

View File

@@ -23,7 +23,10 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -38,32 +41,19 @@ public class DefaultSimpleCacheTest
private DefaultSimpleCache<Integer, String> cache; private DefaultSimpleCache<Integer, String> cache;
@Before @Before
public void setUp() public void setUp() throws Exception
{ {
cache = new DefaultSimpleCache<Integer, String>(); cache = new DefaultSimpleCache<Integer, String>();
cache.setMaxItems(100);
cache.afterPropertiesSet();
} }
@Test @Test
public void unboundedSizeCache() public void boundedSizeCache() throws Exception
{
cache.put(1, "1");
cache.put(2, "2");
cache.put(3, "3");
cache.put(4, "4");
cache.put(5, "5");
assertEquals("1", cache.get(1));
assertEquals("2", cache.get(2));
assertEquals("3", cache.get(3));
assertEquals("4", cache.get(4));
assertEquals("5", cache.get(5));
}
@Test
public void boundedSizeCache()
{ {
// We'll only keep the LAST 3 items // We'll only keep the LAST 3 items
cache.setMaxItems(3); cache.setMaxItems(3);
cache.afterPropertiesSet();
cache.put(1, "1"); cache.put(1, "1");
cache.put(2, "2"); cache.put(2, "2");
@@ -136,7 +126,10 @@ public class DefaultSimpleCacheTest
cache.put(12, "red"); cache.put(12, "red");
cache.put(43, "olive"); cache.put(43, "olive");
Iterator<Integer> it = cache.getKeys().iterator(); List<Integer> keys = new ArrayList<Integer>(cache.getKeys());
Collections.sort(keys);
Iterator<Integer> it = keys.iterator();
assertEquals(3, it.next().intValue()); assertEquals(3, it.next().intValue());
assertEquals(12, it.next().intValue()); assertEquals(12, it.next().intValue());
assertEquals(43, it.next().intValue()); assertEquals(43, it.next().intValue());
@@ -144,14 +137,20 @@ public class DefaultSimpleCacheTest
} }
@Test @Test
public void clearUponSetMaxItems() public void noConcurrentModificationException()
{ {
cache.put(1, "1"); cache.put(1, "1");
assertTrue(cache.contains(1)); cache.put(2, "2");
cache.put(3, "3");
cache.put(4, "4");
Iterator<Integer> i = cache.getKeys().iterator();
i.next();
i.next();
cache.setMaxItems(10); cache.put(5, "5");
// The item should have gone. // Causes a ConcurrentModificationException with a java.util.LinkedHashMap
assertFalse(cache.contains(1)); i.next();
} }
} }