/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.util; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * <p>Wrapper around a standard {@link Map} that provides thread safety through the usage of read and write locks. * * <p>All methods that read from the map first acquire a shared claim and all methods that write to the map first * acquire an exclusive claim. This allows many concurrent readers and exactly one writer. This is simple and sufficient * for read-mostly workloads.</p> * * <p>In addition to the Map interface, methods are provided for manually acquiring shared or exclusive, see * {@link #claimShared()} and {@link #claimExclusive()}, are available. These are useful when something with the * underlying map (e.g cleanup) needs to happen in a globally consistent way. */ public class ReadWriteMap<K,V> implements Map<K,V> { public interface ValueCreator<K,V> { V createValueForKey(K key); } private final Map<K,V> map; private final ReadWriteLock rwLock; private final Lock shared; private final Lock exclusive; private ReadWriteMap(Map<K,V> map, boolean isFair) { this.map = map; this.rwLock = new ReentrantReadWriteLock(isFair); this.shared = rwLock.readLock(); this.exclusive = rwLock.writeLock(); } public static <K,V> ReadWriteMap<K,V> wrapNonFair(Map<K,V> map) { return new ReadWriteMap<>(map, false); } public static <K,V> ReadWriteMap<K,V> wrapFair(Map<K,V> map) { return new ReadWriteMap<>(map, true); } // // Manual lock management // /** Claim shared access for an extended period. Must be paired (i.e. try/finally) with {@link #releaseShared()} */ public void claimShared() { shared.lock(); } /** Release previously acquired shared access. */ public void releaseShared() { shared.unlock(); } /** Claim exclusive access for an extended period. Must be paired (i.e. try/finally) with {@link #releaseExclusive()} */ public void claimExclusive() { exclusive.lock(); } /** Release previously acquired exclusive access. */ public void releaseExclusive() { exclusive.unlock(); } /** * Return the map this wrapper was constructed with. Note that using this without first calling * {@link #claimShared()} or {@link #claimExclusive()} removes guarantees provided by this interface. */ public Map<K,V> getWrappedMap() { return map; } /** * Check that the map does not contain the given key, throwing an {@link IllegalStateException} if it does, and then * delegate to {@link #put(Object, Object)}. */ public V putNewKey(K key, V value) { claimExclusive(); try { if(map.containsKey(key)) { throw new IllegalStateException("Expected new key: " + key); } return map.put(key, value); } finally { releaseExclusive(); } } /** * <p>Attempt to get a value by first delegating to {@link #get(Object)}. If the key does not exist, create a new * default value and put it in the map.</p> * * <p>The creation and putting of a new default value is performed under a single exclusive claim such that any * any concurrent callers of this method will only cause 1 value to be created.</p> */ public V getOrCreateAndPut(K key, ValueCreator<K,V> defaultValueCreator) { V value = get(key); if(value == null) { claimExclusive(); try { // Check again under exclusive lock value = map.get(key); if(value == null) { value = defaultValueCreator.createValueForKey(key); map.put(key, value); } } finally { releaseExclusive(); } } return value; } /** * Update a key to a new value if and only if the current value is as given. * @return true if the key was updated to the new value */ public boolean compareAndSet(K key, V expected, V update) { claimExclusive(); try { V current = map.get(key); if(expected != current) { return false; } map.put(key, update); return true; } finally { releaseExclusive(); } } // // Map interface // @Override public int size() { claimShared(); try { return map.size(); } finally { releaseShared(); } } @Override public boolean isEmpty() { claimShared(); try { return map.isEmpty(); } finally { releaseShared(); } } @Override public boolean containsKey(Object key) { claimShared(); try { return map.containsKey(key); } finally { releaseShared(); } } @Override public boolean containsValue(Object value) { claimShared(); try { return map.containsValue(value); } finally { releaseShared(); } } @Override public V get(Object key) { claimShared(); try { return map.get(key); } finally { releaseShared(); } } @Override public V put(K key, V value) { claimExclusive(); try { return map.put(key, value); } finally { releaseExclusive(); } } @Override public V remove(Object key) { claimExclusive(); try { return map.remove(key); } finally { releaseExclusive(); } } @Override public void putAll(Map<? extends K,? extends V> m) { claimExclusive(); try { map.putAll(m); } finally { releaseExclusive(); } } @Override public void clear() { claimExclusive(); try { map.clear(); } finally { releaseExclusive(); } } private static final String UNSUPPORTED_MSG = "Unsupported. Call getWrappedMap after reading JavaDoc."; /** <b>Unsupported</b>. Use {@link #claimShared()} or {@link #claimExclusive()} and then {@link #getWrappedMap()} */ @Override public Set<K> keySet() { throw new UnsupportedOperationException(UNSUPPORTED_MSG); } /** <b>Unsupported</b>. Use {@link #claimShared()} or {@link #claimExclusive()} and then {@link #getWrappedMap()} */ @Override public Collection<V> values() { throw new UnsupportedOperationException(UNSUPPORTED_MSG); } /** <b>Unsupported</b>. Use {@link #claimShared()} or {@link #claimExclusive()} and then {@link #getWrappedMap()} */ @Override public Set<Entry<K,V>> entrySet() { throw new UnsupportedOperationException(UNSUPPORTED_MSG); } @Override public String toString() { claimShared(); try { return map.toString(); } finally { releaseShared(); } } }