ICU-7434 ICU4J SoftCache with CacheValue that can be null (which were not cached before) or hold a direct reference or hold a resettable Reference; CacheValue strength can be set for cache warm-up

X-SVN-Rev: 38734
This commit is contained in:
Markus Scherer 2016-05-12 22:59:11 +00:00
parent 3f27a9e89d
commit 37c12c5961
8 changed files with 291 additions and 88 deletions

View File

@ -0,0 +1,148 @@
/*
*******************************************************************************
* Copyright (C) 2016, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.impl;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import com.ibm.icu.util.ICUException;
/**
* Value type for cache items:
* Holds a value either via a direct reference or via a {@link Reference},
* depending on the current "strength" when {@code getInstance()} was called.
*
* <p>The value is <i>conceptually<i> immutable.
* If it is held via a direct reference, then it is actually immutable.
*
* <p>A {@code Reference} may be cleared (garbage-collected),
* after which {@code get()} returns null.
* It can then be reset via {@code resetIfAbsent()}.
* The new value should be the same as, or equivalent to, the old value.
*
* <p>Null values are supported. They can be distinguished from cleared values
* via {@code isNull()}.
*
* @param <V> Cache instance value type
*/
public abstract class CacheValue<V> {
/**
* "Strength" of holding a value in CacheValue instances.
* The default strength is {@code SOFT}.
*/
public enum Strength {
/**
* Subsequent {@code getInstance()}-created objects
* will hold direct references to their values.
*/
STRONG,
/**
* Subsequent {@code getInstance()}-created objects
* will hold {@link SoftReference}s to their values.
*/
SOFT
};
private static volatile Strength strength = Strength.SOFT;
@SuppressWarnings("rawtypes")
private static final CacheValue NULL_VALUE = new NullValue();
/**
* Changes the "strength" of value references for subsequent {@code getInstance()} calls.
*/
public static void setStrength(Strength strength) { CacheValue.strength = strength; }
/**
* Returns true if the "strength" is set to {@code STRONG}.
*/
public static boolean futureInstancesWillBeStrong() { return strength == Strength.STRONG; }
/**
* Returns a CacheValue instance that holds the value.
* It holds it directly if the value is null or if the current "strength" is {@code STRONG}.
* Otherwise, it holds it via a {@link Reference}.
*/
@SuppressWarnings("unchecked")
public static <V> CacheValue<V> getInstance(V value) {
if (value == null) {
return NULL_VALUE;
}
return strength == Strength.STRONG ? new StrongValue<V>(value) : new SoftValue<V>(value);
}
/**
* Distinguishes a null value from a Reference value that has been cleared.
*
* @return true if this object represents a null value.
*/
public boolean isNull() { return false; }
/**
* Returns the value (which can be null),
* or null if it was held in a Reference and has been cleared.
*/
public abstract V get();
/**
* If the value was held via a {@link Reference} which has been cleared,
* then it is replaced with a new {@link Reference} to the new value,
* and the new value is returned.
* The old and new values should be the same or equivalent.
*
* <p>Otherwise the old value is returned.
*
* @param value Replacement value, for when the current {@link Reference} has been cleared.
* @return The old or new value.
*/
public abstract V resetIfCleared(V value);
private static final class NullValue<V> extends CacheValue<V> {
@Override
public boolean isNull() { return true; }
@Override
public V get() { return null; }
@Override
public V resetIfCleared(V value) {
if (value != null) {
throw new ICUException("resetting a null value to a non-null value");
}
return null;
}
}
private static final class StrongValue<V> extends CacheValue<V> {
private V value;
StrongValue(V value) { this.value = value; }
@Override
public V get() { return value; }
@Override
public V resetIfCleared(V value) {
// value and this.value should be equivalent, but
// we do not require equals() to be implemented appropriately.
return this.value;
}
}
private static final class SoftValue<V> extends CacheValue<V> {
private Reference<V> ref;
SoftValue(V value) { ref = new SoftReference<V>(value); }
@Override
public V get() { return ref.get(); }
@Override
public synchronized V resetIfCleared(V value) {
V oldValue = ref.get();
if (oldValue == null) {
ref = new SoftReference<V>(value);
return value;
} else {
// value and oldValue should be equivalent, but
// we do not require equals() to be implemented appropriately.
return oldValue;
}
}
}
}

View File

@ -102,7 +102,8 @@ class ICUResourceBundleImpl extends ICUResourceBundle {
super(container, key, resource);
String s = wholeBundle.reader.getString(resource);
// Allow the reader cache's SoftReference to do its job.
if (s.length() < ICUResourceBundleReader.LARGE_SIZE / 2) {
if (s.length() < ICUResourceBundleReader.LARGE_SIZE / 2 ||
CacheValue.futureInstancesWillBeStrong()) {
value = s;
}
}

View File

@ -1182,7 +1182,7 @@ public final class ICUResourceBundleReader {
* <p>This cache uses int[] and Object[] arrays to minimize object creation
* and avoid auto-boxing.
*
* <p>Large resource objects are stored in SoftReferences.
* <p>Large resource objects are usually stored in SoftReferences.
*
* <p>For few resources, a small table is used with binary search.
* When more resources are cached, then the data structure changes to be faster
@ -1210,11 +1210,18 @@ public final class ICUResourceBundleReader {
private int levelBitsList;
private Level rootLevel;
private static boolean storeDirectly(int size) {
return size < LARGE_SIZE || CacheValue.futureInstancesWillBeStrong();
}
@SuppressWarnings("unchecked")
private static final Object putIfCleared(Object[] values, int index, Object item, int size) {
Object value = values[index];
if(!(value instanceof SoftReference)) {
assert size < LARGE_SIZE; // Caller should be consistent for each resource.
// The caller should be consistent for each resource,
// that is, create equivalent objects of equal size every time,
// but the CacheValue "strength" may change over time.
// assert size < LARGE_SIZE;
return value;
}
assert size >= LARGE_SIZE;
@ -1222,7 +1229,8 @@ public final class ICUResourceBundleReader {
if(value != null) {
return value;
}
values[index] = new SoftReference<Object>(item);
values[index] = CacheValue.futureInstancesWillBeStrong() ?
item : new SoftReference<Object>(item);
return item;
}
@ -1271,7 +1279,7 @@ public final class ICUResourceBundleReader {
return level.putIfAbsent(key, item, size);
}
keys[index] = key;
values[index] = (size >= LARGE_SIZE) ? new SoftReference<Object>(item) : item;
values[index] = storeDirectly(size) ? item : new SoftReference<Object>(item);
return item;
}
// Collision: Add a child level, move the old item there,
@ -1403,7 +1411,7 @@ public final class ICUResourceBundleReader {
}
++length;
keys[index] = res;
values[index] = (size >= LARGE_SIZE) ? new SoftReference<Object>(item) : item;
values[index] = storeDirectly(size) ? item : new SoftReference<Object>(item);
return item;
} else /* not found && length == SIMPLE_LENGTH */ {
// Grow to become trie-like.

View File

@ -1,105 +1,86 @@
/*
*******************************************************************************
* Copyright (C) 2010-2011, International Business Machines
* Copyright (C) 2010-2016, International Business Machines
* Corporation and others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.impl;
import java.lang.ref.SoftReference;
import java.util.concurrent.ConcurrentHashMap;
/**
* Generic, thread-safe cache implementation, storing SoftReferences to cached instances.
* Generic, thread-safe cache implementation, usually storing cached instances
* in {@link java.lang.ref.Reference}s via {@link CacheValue}s.
* To use, instantiate a subclass which implements the createInstance() method,
* and call get() with the key and the data. The get() call will use the data
* only if it needs to call createInstance(), otherwise the data is ignored.
*
* By using SoftReferences to instances, the Java runtime can release instances
* once they are not used any more at all. If such an instance is then requested again,
* the get() method will call createInstance() again and also create a new SoftReference.
* The cache holds on to its map of keys to SoftReferenced instances forever.
* <p>When caching instances while the CacheValue "strength" is {@code SOFT},
* the Java runtime can later release these instances once they are not used any more at all.
* If such an instance is then requested again,
* the getInstance() method will call createInstance() again and reset the CacheValue.
* The cache holds on to its map of keys to CacheValues forever.
*
* <p>A value can be null if createInstance() returns null.
* In this case, it must do so consistently for the same key and data.
*
* @param <K> Cache lookup key type
* @param <V> Cache instance value type
* @param <V> Cache instance value type (must not be a CacheValue)
* @param <D> Data type for creating a new instance value
*
* @author Markus Scherer, Mark Davis
*/
public abstract class SoftCache<K, V, D> extends CacheBase<K, V, D> {
private ConcurrentHashMap<K, Object> map = new ConcurrentHashMap<K, Object>();
@SuppressWarnings("unchecked")
@Override
public final V getInstance(K key, D data) {
// We synchronize twice, once on the map and once on valueRef,
// We synchronize twice, once in the ConcurrentHashMap and
// once in valueRef.resetIfCleared(value),
// because we prefer the fine-granularity locking of the ConcurrentHashMap
// over coarser locking on the whole cache instance.
// We use a SettableSoftReference (a second level of indirection) because
// We use a CacheValue (a second level of indirection) because
// ConcurrentHashMap.putIfAbsent() never replaces the key's value, and if it were
// a simple SoftReference we would not be able to reset its value after it has been cleared.
// a simple Reference we would not be able to reset its value after it has been cleared.
// (And ConcurrentHashMap.put() always replaces the value, which we don't want either.)
SettableSoftReference<V> valueRef = map.get(key);
V value;
if(valueRef != null) {
synchronized(valueRef) {
value = valueRef.ref.get();
if(value != null) {
return value;
} else {
// The instance has been evicted, its SoftReference cleared.
// Create and set a new instance.
value = createInstance(key, data);
if (value != null) {
valueRef.ref = new SoftReference<V>(value);
}
return value;
}
Object mapValue = map.get(key);
if(mapValue != null) {
if(!(mapValue instanceof CacheValue)) {
// The value was stored directly.
return (V)mapValue;
}
} else /* valueRef == null */ {
// We had never cached an instance for this key.
value = createInstance(key, data);
if (value == null) {
CacheValue<V> cv = (CacheValue<V>)mapValue;
if(cv.isNull()) {
return null;
}
valueRef = map.putIfAbsent(key, new SettableSoftReference<V>(value));
if(valueRef == null) {
V value = cv.get();
if(value != null) {
return value;
}
// The instance has been evicted, its Reference cleared.
// Create and set a new instance.
value = createInstance(key, data);
return cv.resetIfCleared(value);
} else /* valueRef == null */ {
// We had never cached an instance for this key.
V value = createInstance(key, data);
mapValue = (value != null && CacheValue.futureInstancesWillBeStrong()) ?
value : CacheValue.getInstance(value);
mapValue = map.putIfAbsent(key, mapValue);
if(mapValue == null) {
// Normal "put": Our new value is now cached.
return value;
} else {
// Race condition: Another thread beat us to putting a SettableSoftReference
// into the map. Return its value, but just in case the garbage collector
// was aggressive, we also offer our new instance for caching.
return valueRef.setIfAbsent(value);
}
// Race condition: Another thread beat us to putting a CacheValue
// into the map. Return its value, but just in case the garbage collector
// was aggressive, we also offer our new instance for caching.
if(!(mapValue instanceof CacheValue)) {
// The value was stored directly.
return (V)mapValue;
}
CacheValue<V> cv = (CacheValue<V>)mapValue;
return cv.resetIfCleared(value);
}
}
/**
* Value type for cache items: Has a SoftReference which can be set
* to a new value when the SoftReference has been cleared.
* The SoftCache class sometimes accesses the ref field directly.
*
* @param <V> Cache instance value type
*/
private static final class SettableSoftReference<V> {
private SettableSoftReference(V value) {
ref = new SoftReference<V>(value);
}
/**
* If the SoftReference has been cleared, then this replaces it with a new SoftReference
* for the new value and returns the new value; otherwise returns the current
* SoftReference's value.
* @param value Replacement value, for when the current reference has been cleared
* @return The value that is held by the SoftReference, old or new
*/
private synchronized V setIfAbsent(V value) {
V oldValue = ref.get();
if(oldValue == null) {
ref = new SoftReference<V>(value);
return value;
} else {
return oldValue;
}
}
private SoftReference<V> ref; // never null
}
private ConcurrentHashMap<K, SettableSoftReference<V>> map =
new ConcurrentHashMap<K, SettableSoftReference<V>>();
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,67 @@
/*
*******************************************************************************
* Copyright (C) 2016, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.dev.test.impl;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.impl.CacheValue;
import com.ibm.icu.impl.CacheValue.Strength;
public class CacheTest extends TestFmwk {
public CacheTest() {}
public static void main(String[] args) throws Exception {
new CacheTest().run(args);
}
/** Code coverage for CacheValue. */
public void testNullCacheValue() {
CacheValue<Object> nv = CacheValue.getInstance(null);
assertTrue("null CacheValue isNull()", nv.isNull());
assertTrue("null CacheValue get()==null", nv.get() == null);
assertTrue("null CacheValue reset==null", nv.resetIfCleared(null) == null);
try {
Object v = nv.resetIfCleared(this);
fail("null CacheValue reset(not null) should throw an Exception, returned " +
v + " instead");
} catch(Exception expected) {
}
}
/** Code coverage for CacheValue. */
public void testStrongCacheValue() {
boolean wasStrong = CacheValue.futureInstancesWillBeStrong();
CacheValue.setStrength(Strength.STRONG);
assertTrue("setStrength(STRONG).futureInstancesWillBeStrong()",
CacheValue.futureInstancesWillBeStrong());
CacheValue<Object> sv = CacheValue.<Object>getInstance(this);
assertFalse("strong CacheValue not isNull()", sv.isNull());
assertTrue("strong CacheValue get()==same", sv.get() == this);
// A strong CacheValue never changes value.
// The implementation does not check that the new value is equal to the old one,
// or even of equal type, so it does not matter which new value we pass in.
assertTrue("strong CacheValue reset==same", sv.resetIfCleared("") == this);
if (!wasStrong) {
CacheValue.setStrength(Strength.SOFT);
}
}
/** Code coverage for CacheValue. */
public void testSoftCacheValue() {
boolean wasStrong = CacheValue.futureInstancesWillBeStrong();
CacheValue.setStrength(Strength.SOFT);
assertFalse("setStrength(SOFT).futureInstancesWillBeStrong()",
CacheValue.futureInstancesWillBeStrong());
CacheValue<Object> sv = CacheValue.<Object>getInstance(this);
assertFalse("soft CacheValue not isNull()", sv.isNull());
Object v = sv.get();
assertTrue("soft CacheValue get()==same or null", v == this || v == null);
assertTrue("soft CacheValue reset==same", sv.resetIfCleared(this) == this);
if (wasStrong) {
CacheValue.setStrength(Strength.STRONG);
}
}
}

View File

@ -17,18 +17,12 @@ public class TestAll extends TestGroup {
}
public TestAll() {
super("com.ibm.icu.dev.test.util",
new String[] {
"ICUServiceTest",
"ICUServiceThreadTest",
"ICUBinaryTest",
"SimpleFormatterTest",
"TextTrieMapTest"
},
"Test miscellaneous implementation utilities");
super(
new String[] {
"CacheTest"
},
"Test miscellaneous implementation classes");
}
public static final String CLASS_TARGET_NAME = "Impl";
}

View File

@ -19,6 +19,11 @@ public class TestAll extends TestGroup {
public TestAll() {
super(
new String[] {
"ICUServiceTest",
"ICUServiceThreadTest",
"ICUBinaryTest",
"SimpleFormatterTest",
"TextTrieMapTest",
"VersionInfoTest",
"ICUResourceBundleTest",
"BytesTrieTest",