com.google.inject.internal.util.MapMaker Maven / Gradle / Ivy
/*
* Copyright (C) 2009 Google Inc.
*
* 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 com.google.inject.internal.util;
import com.google.inject.internal.util.CustomConcurrentHashMap.ComputingStrategy;
import com.google.inject.internal.util.CustomConcurrentHashMap.Internals;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* A {@link ConcurrentMap} builder, providing any combination of these
* features: {@linkplain SoftReference soft} or {@linkplain WeakReference
* weak} keys, soft or weak values, timed expiration, and on-demand
* computation of values. Usage example: {@code
*
* ConcurrentMap graphs = new MapMaker()
* .concurrencyLevel(32)
* .softKeys()
* .weakValues()
* .expiration(30, TimeUnit.MINUTES)
* .makeComputingMap(
* new Function() {
* public Graph apply(Key key) {
* return createExpensiveGraph(key);
* }
* });}
*
* These features are all optional; {@code new MapMaker().makeMap()}
* returns a valid concurrent map that behaves exactly like a
* {@link ConcurrentHashMap}.
*
* The returned map is implemented as a hash table with similar performance
* characteristics to {@link ConcurrentHashMap}. It supports all optional
* operations of the {@code ConcurrentMap} interface. It does not permit
* null keys or values. It is serializable; however, serializing a map that
* uses soft or weak references can give unpredictable results.
*
* Note: by default, the returned map uses equality comparisons
* (the {@link Object#equals(Object) equals} method) to determine equality
* for keys or values. However, if {@link #weakKeys()} or {@link
* #softKeys()} was specified, the map uses identity ({@code ==})
* comparisons instead for keys. Likewise, if {@link #weakValues()} or
* {@link #softValues()} was specified, the map uses identity comparisons
* for values.
*
*
The returned map has weakly consistent iteration: an iterator
* over one of the map's view collections may reflect some, all or none of
* the changes made to the map after the iterator was created.
*
*
An entry whose key or value is reclaimed by the garbage collector
* immediately disappears from the map. (If the default settings of strong
* keys and strong values are used, this will never happen.) The client can
* never observe a partially-reclaimed entry. Any {@link java.util.Map.Entry}
* instance retrieved from the map's {@linkplain Map#entrySet() entry set}
* is snapshot of that entry's state at the time of retrieval.
*
*
{@code new MapMaker().weakKeys().makeMap()} can almost always be
* used as a drop-in replacement for {@link java.util.WeakHashMap}, adding
* concurrency, asynchronous cleanup, identity-based equality for keys, and
* great flexibility.
*
* @author Bob Lee
* @author Kevin Bourrillion
*/
public final class MapMaker {
private Strength keyStrength = Strength.STRONG;
private Strength valueStrength = Strength.STRONG;
private long expirationNanos = 0;
private boolean useCustomMap;
private final CustomConcurrentHashMap.Builder builder
= new CustomConcurrentHashMap.Builder();
/**
* Constructs a new {@code MapMaker} instance with default settings,
* including strong keys, strong values, and no automatic expiration.
*/
public MapMaker() {}
/**
* Sets a custom initial capacity (defaults to 16). Resizing this or
* any other kind of hash table is a relatively slow operation, so,
* when possible, it is a good idea to provide estimates of expected
* table sizes.
*
* @throws IllegalArgumentException if {@code initialCapacity} is
* negative
* @throws IllegalStateException if an initial capacity was already set
* (TODO: make that true)
*/
public MapMaker initialCapacity(int initialCapacity) {
builder.initialCapacity(initialCapacity);
return this;
}
/**
* Sets a custom load factor (defaults to 0.75).
*
* @throws IllegalArgumentException if {@code loadFactor} is
* nonpositive
* @throws IllegalStateException if a load factor was already set
* (TODO: make that true)
*/
public MapMaker loadFactor(float loadFactor) {
builder.loadFactor(loadFactor);
return this;
}
/**
* Guides the allowed concurrency among update operations. Used as a
* hint for internal sizing. The table is internally partitioned to try
* to permit the indicated number of concurrent updates without
* contention. Because placement in hash tables is essentially random,
* the actual concurrency will vary. Ideally, you should choose a value
* to accommodate as many threads as will ever concurrently modify the
* table. Using a significantly higher value than you need can waste
* space and time, and a significantly lower value can lead to thread
* contention. But overestimates and underestimates within an order of
* magnitude do not usually have much noticeable impact. A value of one
* is appropriate when it is known that only one thread will modify and
* all others will only read. Defaults to 16.
*
* @throws IllegalArgumentException if {@code concurrencyLevel} is
* nonpositive
* @throws IllegalStateException if a concurrency level was already set
* (TODO: make that true)
*/
public MapMaker concurrencyLevel(int concurrencyLevel) {
builder.concurrencyLevel(concurrencyLevel);
return this;
}
/**
* Specifies that each key (not value) stored in the map should be
* wrapped in a {@link WeakReference} (by default, strong references
* are used).
*
* @throws IllegalStateException if the key strength was already set
*/
public MapMaker weakKeys() {
return setKeyStrength(Strength.WEAK);
}
/**
* Specifies that each key (not value) stored in the map should be
* wrapped in a {@link SoftReference} (by default, strong references
* are used).
*
* @throws IllegalStateException if the key strength was already set
*/
public MapMaker softKeys() {
return setKeyStrength(Strength.SOFT);
}
private MapMaker setKeyStrength(Strength strength) {
if (keyStrength != Strength.STRONG) {
throw new IllegalStateException("Key strength was already set to "
+ keyStrength + ".");
}
keyStrength = strength;
useCustomMap = true;
return this;
}
/**
* Specifies that each value (not key) stored in the map should be
* wrapped in a {@link WeakReference} (by default, strong references
* are used).
*
* @throws IllegalStateException if the key strength was already set
*/
public MapMaker weakValues() {
return setValueStrength(Strength.WEAK);
}
/**
* Specifies that each value (not key) stored in the map should be
* wrapped in a {@link SoftReference} (by default, strong references
* are used).
*
* @throws IllegalStateException if the value strength was already set
*/
public MapMaker softValues() {
return setValueStrength(Strength.SOFT);
}
private MapMaker setValueStrength(Strength strength) {
if (valueStrength != Strength.STRONG) {
throw new IllegalStateException("Value strength was already set to "
+ valueStrength + ".");
}
valueStrength = strength;
useCustomMap = true;
return this;
}
/**
* Specifies that each entry should be automatically removed from the
* map once a fixed duration has passed since the entry's creation.
*
* @param duration the length of time after an entry is created that it
* should be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @throws IllegalArgumentException if {@code duration} is not positive
* @throws IllegalStateException if the expiration time was already set
*/
public MapMaker expiration(long duration, TimeUnit unit) {
if (expirationNanos != 0) {
throw new IllegalStateException("expiration time of "
+ expirationNanos + " ns was already set");
}
if (duration <= 0) {
throw new IllegalArgumentException("invalid duration: " + duration);
}
this.expirationNanos = unit.toNanos(duration);
useCustomMap = true;
return this;
}
/**
* Builds the final map, without on-demand computation of values.
*
* @param the type of keys to be stored in the returned map
* @param the type of values to be stored in the returned map
* @return a concurrent map having the requested features
*/
public ConcurrentMap makeMap() {
return useCustomMap
? new StrategyImpl(this).map
: new ConcurrentHashMap(builder.initialCapacity,
builder.loadFactor, builder.concurrencyLevel);
}
/**
* Builds a map that supports atomic, on-demand computation of values. {@link
* Map#get} returns the value corresponding to the given key, atomically
* computes it using the computer function passed to this builder, or waits
* for another thread to compute the value if necessary. Only one value will
* be computed for each key at a given time.
*
* If an entry's value has not finished computing yet, query methods
* besides {@link java.util.Map#get} return immediately as if an entry doesn't
* exist. In other words, an entry isn't externally visible until the value's
* computation completes.
*
*
{@link Map#get} in the returned map implementation throws:
*
*
* - {@link NullPointerException} if the key is null or the computer returns
* null
* - or {@link ComputationException} wrapping an exception thrown by the
* computation
*
*
* Note: Callers of {@code get()} must ensure that the key
* argument is of type {@code K}. {@code Map.get()} takes {@code Object}, so
* the key type is not checked at compile time. Passing an object of a type
* other than {@code K} can result in that object being unsafely passed to the
* computer function as type {@code K} not to mention the unsafe key being
* stored in the map.
*
*
If {@link java.util.Map#put} is called before a computation completes,
* other threads waiting on the computation will wake up and return the put
* value up until the computation completes, at which point the computation
* result will overwrite the value from the {@code put} in the map.
*/
public ConcurrentMap makeComputingMap(
Function super K, ? extends V> computer) {
return new StrategyImpl(this, computer).map;
}
// Remainder of this file is private implementation details
private enum Strength {
WEAK {
boolean equal(Object a, Object b) {
return a == b;
}
int hash(Object o) {
return System.identityHashCode(o);
}
ValueReference referenceValue(
ReferenceEntry entry, V value) {
return new WeakValueReference(value, entry);
}
ReferenceEntry newEntry(
Internals> internals, K key,
int hash, ReferenceEntry next) {
return (next == null)
? new WeakEntry(internals, key, hash)
: new LinkedWeakEntry(internals, key, hash, next);
}
ReferenceEntry copyEntry(
K key, ReferenceEntry original,
ReferenceEntry newNext) {
WeakEntry from = (WeakEntry) original;
return (newNext == null)
? new WeakEntry(from.internals, key, from.hash)
: new LinkedWeakEntry(
from.internals, key, from.hash, newNext);
}
},
SOFT {
boolean equal(Object a, Object b) {
return a == b;
}
int hash(Object o) {
return System.identityHashCode(o);
}
ValueReference referenceValue(
ReferenceEntry entry, V value) {
return new SoftValueReference(value, entry);
}
ReferenceEntry newEntry(
Internals> internals, K key,
int hash, ReferenceEntry next) {
return (next == null)
? new SoftEntry(internals, key, hash)
: new LinkedSoftEntry(internals, key, hash, next);
}
ReferenceEntry copyEntry(
K key, ReferenceEntry original,
ReferenceEntry newNext) {
SoftEntry from = (SoftEntry) original;
return (newNext == null)
? new SoftEntry(from.internals, key, from.hash)
: new LinkedSoftEntry(
from.internals, key, from.hash, newNext);
}
},
STRONG {
boolean equal(Object a, Object b) {
return a.equals(b);
}
int hash(Object o) {
return o.hashCode();
}
ValueReference referenceValue(
ReferenceEntry entry, V value) {
return new StrongValueReference(value);
}
ReferenceEntry newEntry(
Internals> internals, K key,
int hash, ReferenceEntry next) {
return (next == null)
? new StrongEntry(internals, key, hash)
: new LinkedStrongEntry(
internals, key, hash, next);
}
ReferenceEntry copyEntry(
K key, ReferenceEntry original,
ReferenceEntry newNext) {
StrongEntry from = (StrongEntry) original;
return (newNext == null)
? new StrongEntry(from.internals, key, from.hash)
: new LinkedStrongEntry(
from.internals, key, from.hash, newNext);
}
};
/**
* Determines if two keys or values are equal according to this
* strength strategy.
*/
abstract boolean equal(Object a, Object b);
/**
* Hashes a key according to this strategy.
*/
abstract int hash(Object o);
/**
* Creates a reference for the given value according to this value
* strength.
*/
abstract ValueReference referenceValue(
ReferenceEntry entry, V value);
/**
* Creates a new entry based on the current key strength.
*/
abstract ReferenceEntry newEntry(
Internals> internals, K key,
int hash, ReferenceEntry next);
/**
* Creates a new entry and copies the value and other state from an
* existing entry.
*/
abstract ReferenceEntry copyEntry(K key,
ReferenceEntry original, ReferenceEntry newNext);
}
private static class StrategyImpl implements Serializable,
ComputingStrategy> {
final Strength keyStrength;
final Strength valueStrength;
final ConcurrentMap map;
final long expirationNanos;
Internals> internals;
StrategyImpl(MapMaker maker) {
this.keyStrength = maker.keyStrength;
this.valueStrength = maker.valueStrength;
this.expirationNanos = maker.expirationNanos;
map = maker.builder.buildMap(this);
}
StrategyImpl(
MapMaker maker, Function super K, ? extends V> computer) {
this.keyStrength = maker.keyStrength;
this.valueStrength = maker.valueStrength;
this.expirationNanos = maker.expirationNanos;
map = maker.builder.buildComputingMap(this, computer);
}
public void setValue(ReferenceEntry entry, V value) {
setValueReference(
entry, valueStrength.referenceValue(entry, value));
if (expirationNanos > 0) {
scheduleRemoval(entry.getKey(), value);
}
}
void scheduleRemoval(K key, V value) {
/*
* TODO: Keep weak reference to map, too. Build a priority
* queue out of the entries themselves instead of creating a
* task per entry. Then, we could have one recurring task per
* map (which would clean the entire map and then reschedule
* itself depending upon when the next expiration comes). We
* also want to avoid removing an entry prematurely if the
* entry was set to the same value again.
*/
final WeakReference keyReference = new WeakReference(key);
final WeakReference valueReference = new WeakReference(value);
ExpirationTimer.instance.schedule(
new TimerTask() {
public void run() {
K key = keyReference.get();
if (key != null) {
// Remove if the value is still the same.
map.remove(key, valueReference.get());
}
}
}, TimeUnit.NANOSECONDS.toMillis(expirationNanos));
}
public boolean equalKeys(K a, Object b) {
return keyStrength.equal(a, b);
}
public boolean equalValues(V a, Object b) {
return valueStrength.equal(a, b);
}
public int hashKey(Object key) {
return keyStrength.hash(key);
}
public K getKey(ReferenceEntry entry) {
return entry.getKey();
}
public int getHash(ReferenceEntry entry) {
return entry.getHash();
}
public ReferenceEntry newEntry(
K key, int hash, ReferenceEntry next) {
return keyStrength.newEntry(internals, key, hash, next);
}
public ReferenceEntry copyEntry(K key,
ReferenceEntry original, ReferenceEntry newNext) {
ValueReference valueReference = original.getValueReference();
if (valueReference == COMPUTING) {
ReferenceEntry newEntry
= newEntry(key, original.getHash(), newNext);
newEntry.setValueReference(
new FutureValueReference(original, newEntry));
return newEntry;
} else {
ReferenceEntry newEntry
= newEntry(key, original.getHash(), newNext);
newEntry.setValueReference(valueReference.copyFor(newEntry));
return newEntry;
}
}
/**
* Waits for a computation to complete. Returns the result of the
* computation or null if none was available.
*/
public V waitForValue(ReferenceEntry entry)
throws InterruptedException {
ValueReference valueReference = entry.getValueReference();
if (valueReference == COMPUTING) {
synchronized (entry) {
while ((valueReference = entry.getValueReference())
== COMPUTING) {
entry.wait();
}
}
}
return valueReference.waitForValue();
}
/**
* Used by CustomConcurrentHashMap to retrieve values. Returns null
* instead of blocking or throwing an exception.
*/
public V getValue(ReferenceEntry entry) {
ValueReference valueReference = entry.getValueReference();
return valueReference.get();
}
public V compute(K key, final ReferenceEntry entry,
Function super K, ? extends V> computer) {
V value;
try {
value = computer.apply(key);
} catch (Throwable t) {
setValueReference(
entry, new ComputationExceptionReference(t));
throw new ComputationException(t);
}
if (value == null) {
String message
= computer + " returned null for key " + key + ".";
setValueReference(
entry, new NullOutputExceptionReference(message));
throw new NullOutputException(message);
} else {
setValue(entry, value);
}
return value;
}
/**
* Sets the value reference on an entry and notifies waiting
* threads.
*/
void setValueReference(ReferenceEntry entry,
ValueReference valueReference) {
boolean notifyOthers = (entry.getValueReference() == COMPUTING);
entry.setValueReference(valueReference);
if (notifyOthers) {
synchronized (entry) {
entry.notifyAll();
}
}
}
/**
* Points to an old entry where a value is being computed. Used to
* support non-blocking copying of entries during table expansion,
* removals, etc.
*/
private class FutureValueReference implements ValueReference {
final ReferenceEntry original;
final ReferenceEntry newEntry;
FutureValueReference(
ReferenceEntry original, ReferenceEntry newEntry) {
this.original = original;
this.newEntry = newEntry;
}
public V get() {
boolean success = false;
try {
V value = original.getValueReference().get();
success = true;
return value;
} finally {
if (!success) {
removeEntry();
}
}
}
public ValueReference copyFor(ReferenceEntry entry) {
return new FutureValueReference(original, entry);
}
public V waitForValue() throws InterruptedException {
boolean success = false;
try {
// assert that key != null
V value = StrategyImpl.this.waitForValue(original);
success = true;
return value;
} finally {
if (!success) {
removeEntry();
}
}
}
/**
* Removes the entry in the event of an exception. Ideally,
* we'd clean up as soon as the computation completes, but we
* can't do that without keeping a reference to this entry from
* the original.
*/
void removeEntry() {
internals.removeEntry(newEntry);
}
}
public ReferenceEntry getNext(
ReferenceEntry entry) {
return entry.getNext();
}
public void setInternals(
Internals> internals) {
this.internals = internals;
}
private static final long serialVersionUID = 0;
private void writeObject(ObjectOutputStream out)
throws IOException {
// Custom serialization code ensures that the key and value
// strengths are written before the map. We'll need them to
// deserialize the map entries.
out.writeObject(keyStrength);
out.writeObject(valueStrength);
out.writeLong(expirationNanos);
// TODO: It is possible for the strategy to try to use the map
// or internals during deserialization, for example, if an
// entry gets reclaimed. We could detect this case and queue up
// removals to be flushed after we deserialize the map.
out.writeObject(internals);
out.writeObject(map);
}
/**
* Fields used during deserialization. We use a nested class so we
* don't load them until we need them. We need to use reflection to
* set final fields outside of the constructor.
*/
private static class Fields {
static final Field keyStrength = findField("keyStrength");
static final Field valueStrength = findField("valueStrength");
static final Field expirationNanos = findField("expirationNanos");
static final Field internals = findField("internals");
static final Field map = findField("map");
static Field findField(String name) {
try {
Field f = StrategyImpl.class.getDeclaredField(name);
f.setAccessible(true);
return f;
} catch (NoSuchFieldException e) {
throw new AssertionError(e);
}
}
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
try {
Fields.keyStrength.set(this, in.readObject());
Fields.valueStrength.set(this, in.readObject());
Fields.expirationNanos.set(this, in.readLong());
Fields.internals.set(this, in.readObject());
Fields.map.set(this, in.readObject());
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
}
/** A reference to a value. */
private interface ValueReference {
/**
* Gets the value. Does not block or throw exceptions.
*/
V get();
/** Creates a copy of this reference for the given entry. */
ValueReference copyFor(ReferenceEntry entry);
/**
* Waits for a value that may still be computing. Unlike get(),
* this method can block (in the case of FutureValueReference) or
* throw an exception.
*/
V waitForValue() throws InterruptedException;
}
private static final ValueReference