All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.inject.internal.util.MapMaker Maven / Gradle / Ivy

There is a newer version: 7.0.0
Show newest version
/*
 * 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 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 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 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 COMPUTING = new ValueReference() { public Object get() { return null; } public ValueReference copyFor( ReferenceEntry entry) { throw new AssertionError(); } public Object waitForValue() { throw new AssertionError(); } }; /** * Singleton placeholder that indicates a value is being computed. */ @SuppressWarnings("unchecked") // Safe because impl never uses a parameter or returns any non-null value private static ValueReference computing() { return (ValueReference) COMPUTING; } /** Used to provide null output exceptions to other threads. */ private static class NullOutputExceptionReference implements ValueReference { final String message; NullOutputExceptionReference(String message) { this.message = message; } public V get() { return null; } public ValueReference copyFor( ReferenceEntry entry) { return this; } public V waitForValue() { throw new NullOutputException(message); } } /** Used to provide computation exceptions to other threads. */ private static class ComputationExceptionReference implements ValueReference { final Throwable t; ComputationExceptionReference(Throwable t) { this.t = t; } public V get() { return null; } public ValueReference copyFor( ReferenceEntry entry) { return this; } public V waitForValue() { throw new AsynchronousComputationException(t); } } /** Wrapper class ensures that queue isn't created until it's used. */ private static class QueueHolder { static final FinalizableReferenceQueue queue = new FinalizableReferenceQueue(); } /** * An entry in a reference map. */ private interface ReferenceEntry { /** * Gets the value reference from this entry. */ ValueReference getValueReference(); /** * Sets the value reference for this entry. * * @param valueReference */ void setValueReference(ValueReference valueReference); /** * Removes this entry from the map if its value reference hasn't * changed. Used to clean up after values. The value reference can * just call this method on the entry so it doesn't have to keep * its own reference to the map. */ void valueReclaimed(); /** Gets the next entry in the chain. */ ReferenceEntry getNext(); /** Gets the entry's hash. */ int getHash(); /** Gets the key for this entry. */ public K getKey(); } /** * Used for strongly-referenced keys. */ private static class StrongEntry implements ReferenceEntry { final K key; StrongEntry(Internals> internals, K key, int hash) { this.internals = internals; this.key = key; this.hash = hash; } public K getKey() { return this.key; } // The code below is exactly the same for each entry type. final Internals> internals; final int hash; volatile ValueReference valueReference = computing(); public ValueReference getValueReference() { return valueReference; } public void setValueReference( ValueReference valueReference) { this.valueReference = valueReference; } public void valueReclaimed() { internals.removeEntry(this, null); } public ReferenceEntry getNext() { return null; } public int getHash() { return hash; } } private static class LinkedStrongEntry extends StrongEntry { LinkedStrongEntry(Internals> internals, K key, int hash, ReferenceEntry next) { super(internals, key, hash); this.next = next; } final ReferenceEntry next; @Override public ReferenceEntry getNext() { return next; } } /** * Used for softly-referenced keys. */ private static class SoftEntry extends FinalizableSoftReference implements ReferenceEntry { SoftEntry(Internals> internals, K key, int hash) { super(key, QueueHolder.queue); this.internals = internals; this.hash = hash; } public K getKey() { return get(); } public void finalizeReferent() { internals.removeEntry(this); } // The code below is exactly the same for each entry type. final Internals> internals; final int hash; volatile ValueReference valueReference = computing(); public ValueReference getValueReference() { return valueReference; } public void setValueReference( ValueReference valueReference) { this.valueReference = valueReference; } public void valueReclaimed() { internals.removeEntry(this, null); } public ReferenceEntry getNext() { return null; } public int getHash() { return hash; } } private static class LinkedSoftEntry extends SoftEntry { LinkedSoftEntry(Internals> internals, K key, int hash, ReferenceEntry next) { super(internals, key, hash); this.next = next; } final ReferenceEntry next; @Override public ReferenceEntry getNext() { return next; } } /** * Used for weakly-referenced keys. */ private static class WeakEntry extends FinalizableWeakReference implements ReferenceEntry { WeakEntry(Internals> internals, K key, int hash) { super(key, QueueHolder.queue); this.internals = internals; this.hash = hash; } public K getKey() { return get(); } public void finalizeReferent() { internals.removeEntry(this); } // The code below is exactly the same for each entry type. final Internals> internals; final int hash; volatile ValueReference valueReference = computing(); public ValueReference getValueReference() { return valueReference; } public void setValueReference( ValueReference valueReference) { this.valueReference = valueReference; } public void valueReclaimed() { internals.removeEntry(this, null); } public ReferenceEntry getNext() { return null; } public int getHash() { return hash; } } private static class LinkedWeakEntry extends WeakEntry { LinkedWeakEntry(Internals> internals, K key, int hash, ReferenceEntry next) { super(internals, key, hash); this.next = next; } final ReferenceEntry next; @Override public ReferenceEntry getNext() { return next; } } /** References a weak value. */ private static class WeakValueReference extends FinalizableWeakReference implements ValueReference { final ReferenceEntry entry; WeakValueReference(V referent, ReferenceEntry entry) { super(referent, QueueHolder.queue); this.entry = entry; } public void finalizeReferent() { entry.valueReclaimed(); } public ValueReference copyFor( ReferenceEntry entry) { return new WeakValueReference(get(), entry); } public V waitForValue() throws InterruptedException { return get(); } } /** References a soft value. */ private static class SoftValueReference extends FinalizableSoftReference implements ValueReference { final ReferenceEntry entry; SoftValueReference(V referent, ReferenceEntry entry) { super(referent, QueueHolder.queue); this.entry = entry; } public void finalizeReferent() { entry.valueReclaimed(); } public ValueReference copyFor( ReferenceEntry entry) { return new SoftValueReference(get(), entry); } public V waitForValue() { return get(); } } /** References a strong value. */ private static class StrongValueReference implements ValueReference { final V referent; StrongValueReference(V referent) { this.referent = referent; } public V get() { return referent; } public ValueReference copyFor( ReferenceEntry entry) { return this; } public V waitForValue() { return get(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy