/* * Copyright (c) 2013-2017 Cinchapi 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.cinchapi.common.util; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.locks.StampedLock; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * An {@link IncrementalSortMap} is a {@link ConcurrentNavigableMap} that is * optimized for adding elements in constant time while enjoying the benefits of * a typical sorted collection. * <p> * Internally, this map initially stores elements in several hashmaps. The * elements are incrementally sorted only when a call is made to a method that * requires the elements to be in sorted order. * </p> * <p> * The benefit to this approach is that writes to the map are very fast (e.g. * constant time). Retrievals <em>may</em> occur in constant time (if the key * has not yet been sorted), but never perform any worse than retrieval times * for typical sorted collections. With that said, we do make tradeoffs in cases * where there is a requirement to get a sorted view of the map or perform an * operation in the {@link java.util.NavigableMap NavigableMap} interface. In * those cases, all the unsorted elements in the map, must be sorted first and * then the traversal must happen. * </p> * * @author Jeff Nelson */ @ThreadSafe public class IncrementalSortMap<K, V> implements ConcurrentNavigableMap<K, V> { /** * Return a new {@link IncrementalSortMap} that sorts elements based on * their natural order. * * @return the IncrementalSortMap */ public static <K, V> IncrementalSortMap<K, V> create() { return new IncrementalSortMap<K, V>(null); } /** * Return an {@link IncrementalSortMap} that sorts elements based on the * rules of the specified {@code comparator}. * * @param comparator * @return the IncrementalSortMap */ public static <K, V> IncrementalSortMap<K, V> create( Comparator<? super K> comparator) { return new IncrementalSortMap<K, V>(comparator); } /** * The number of internal segments where data is initially written. More * segments also us to have higher throughput, but too many can lead to way * too much overhead. */ private final int concurrencyLevel = 16; // TODO maybe make this // configurable? /** * The locks that are used to control concurrent access to each segment. */ private final StampedLock[] segmentLocks; /** * The segments where data is initially written. Each key is assigned to a * unique segment, and written there before eventually being placed in the * {@link #sorted} map. */ private final HashMap<K, V>[] segments; /** * The sorted collection of data which is incrementally populated from the * unsorted {@link #segments}. */ private final ConcurrentSkipListMap<K, V> sorted; /** * Construct a new instance. * * @param comparator */ @SuppressWarnings("unchecked") private IncrementalSortMap(Comparator<? super K> comparator) { this.sorted = comparator == null ? new ConcurrentSkipListMap<K, V>() : new ConcurrentSkipListMap<K, V>(comparator); this.segments = new HashMap[concurrencyLevel]; this.segmentLocks = new StampedLock[concurrencyLevel]; for (int i = 0; i < concurrencyLevel; i++) { segments[i] = Maps.<K, V> newHashMap(); segmentLocks[i] = new StampedLock(); } } @Override public java.util.Map.Entry<K, V> ceilingEntry(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.ceilingEntry(key); } finally { releaseSegmentLocks(stamps); } } @Override public K ceilingKey(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.ceilingKey(key); } finally { releaseSegmentLocks(stamps); } } @Override public void clear() { long[] stamps = grabAllSegmentWriteLocks(); try { sorted.clear(); for (HashMap<K, V> segment : segments) { segment.clear(); } } finally { releaseSegmentLocks(stamps); } } @Override public Comparator<? super K> comparator() { return sorted.comparator(); } @Override public boolean containsKey(Object key) { int seg = getSegment(key); long stamp = segmentLocks[seg].readLock(); try { return segments[seg].containsKey(key) || sorted.containsKey(key); } finally { segmentLocks[seg].unlock(stamp); } } @Override public boolean containsValue(Object value) { long[] stamps = grabAllSegmentReadLocks(); try { for (HashMap<K, V> segment : segments) { if(segment.containsValue(value)) { return true; } } return sorted.containsValue(value); } finally { releaseSegmentLocks(stamps); } } @Override public NavigableSet<K> descendingKeySet() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.descendingKeySet(); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> descendingMap() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.descendingMap(); } finally { releaseSegmentLocks(stamps); } } @Override public Set<java.util.Map.Entry<K, V>> entrySet() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.entrySet(); } finally { releaseSegmentLocks(stamps); } } @SuppressWarnings("rawtypes") @Override public boolean equals(Object obj) { if(obj instanceof IncrementalSortMap) { IncrementalSortMap other = (IncrementalSortMap) obj; long[] stamps = grabAllSegmentReadLocks(); long[] otherStamps = other.grabAllSegmentReadLocks(); try { sort(); other.sort(); return sorted.equals(other.sorted); } finally { releaseSegmentLocks(stamps); other.releaseSegmentLocks(otherStamps); } } else { return false; } } @Override public java.util.Map.Entry<K, V> firstEntry() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.firstEntry(); } finally { releaseSegmentLocks(stamps); } } @Override public K firstKey() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.firstKey(); } finally { releaseSegmentLocks(stamps); } } @Override public java.util.Map.Entry<K, V> floorEntry(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.floorEntry(key); } finally { releaseSegmentLocks(stamps); } } @Override public K floorKey(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.floorKey(key); } finally { releaseSegmentLocks(stamps); } } @Override public V get(Object key) { int seg = getSegment(key); long stamp = segmentLocks[seg].readLock(); try { V val = segments[seg].get(key); if(val == null) { val = sorted.get(key); } return val; } finally { segmentLocks[seg].unlock(stamp); } } @Override public int hashCode() { long[] stamps = grabAllSegmentReadLocks(); try { sort(); return sorted.hashCode(); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> headMap(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.headMap(key); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> headMap(K toKey, boolean inclusive) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.headMap(toKey, inclusive); } finally { releaseSegmentLocks(stamps); } } @Override public java.util.Map.Entry<K, V> higherEntry(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.higherEntry(key); } finally { releaseSegmentLocks(stamps); } } @Override public K higherKey(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.higherKey(key); } finally { releaseSegmentLocks(stamps); } } @Override public boolean isEmpty() { long[] stamps = grabAllSegmentReadLocks(); try { for (HashMap<K, V> segment : segments) { if(!segment.isEmpty()) { return false; } } return sorted.isEmpty(); } finally { releaseSegmentLocks(stamps); } } @Override public NavigableSet<K> keySet() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.keySet(); } finally { releaseSegmentLocks(stamps); } } @Override public java.util.Map.Entry<K, V> lastEntry() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.lastEntry(); } finally { releaseSegmentLocks(stamps); } } @Override public K lastKey() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.lastKey(); } finally { releaseSegmentLocks(stamps); } } @Override public java.util.Map.Entry<K, V> lowerEntry(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.lowerEntry(key); } finally { releaseSegmentLocks(stamps); } } @Override public K lowerKey(K key) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.lowerKey(key); } finally { releaseSegmentLocks(stamps); } } @Override public NavigableSet<K> navigableKeySet() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.navigableKeySet(); } finally { releaseSegmentLocks(stamps); } } @Override public java.util.Map.Entry<K, V> pollFirstEntry() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.pollFirstEntry(); } finally { releaseSegmentLocks(stamps); } } @Override public java.util.Map.Entry<K, V> pollLastEntry() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.pollLastEntry(); } finally { releaseSegmentLocks(stamps); } } /** * This implementation departs from the normal contract specified in the * {@link Map} implementation because this method ALWAYS returns * {@code null}. * * @param key * @param value * @return {@code null} */ @Override @Nullable public V put(K key, V value) { int seg = getSegment(key); long stamp = segmentLocks[seg].writeLock(); try { segments[seg].put(key, value); return null; // always return null because we don't want to look in // the sorted map to get the old value for the key // because that would unnecessarily slow down the write // process. } finally { segmentLocks[seg].unlock(stamp); } } @Override public void putAll(Map<? extends K, ? extends V> m) { long[] stamps = grabAllSegmentWriteLocks(); try { if(m instanceof SortedMap) { sorted.putAll(m); } else { for (Entry<? extends K, ? extends V> entry : m.entrySet()) { int seg = getSegment(entry.getKey()); segments[seg].put(entry.getKey(), entry.getValue()); } } } finally { releaseSegmentLocks(stamps); } } @Override public V putIfAbsent(K key, V value) { int seg = getSegment(key); long stamp = segmentLocks[seg].writeLock(); try { V v = segments[seg].get(key); v = v == null ? sorted.get(key) : v; if(!value.equals(v)) { segments[seg].put(key, value); } return v; } finally { segmentLocks[seg].unlock(stamp); } } @Override public V remove(Object key) { int seg = getSegment(key); long stamp = segmentLocks[seg].writeLock(); try { V a = segments[seg].remove(key); V b = sorted.remove(key); try { return MoreObjects.firstNonNull(a, b); } catch (NullPointerException e) { return null; } } finally { segmentLocks[seg].unlock(stamp); } } @Override public boolean remove(Object key, Object value) { int seg = getSegment(key); long stamp = segmentLocks[seg].writeLock(); try { V v = segments[seg].get(key); v = v == null ? sorted.get(key) : v; if(value != null && value.equals(v)) { segments[seg].remove(key); sorted.remove(key); return true; } else { return false; } } finally { segmentLocks[seg].unlock(stamp); } } @Override public V replace(K key, V value) { int seg = getSegment(key); long stamp = segmentLocks[seg].writeLock(); try { V v = segments[seg].get(key); v = v == null ? sorted.get(key) : v; if(v != null) { segments[seg].put(key, value); } return v; } finally { segmentLocks[seg].unlock(stamp); } } @Override public boolean replace(K key, V oldValue, V newValue) { int seg = getSegment(key); long stamp = segmentLocks[seg].writeLock(); try { V v = segments[seg].get(key); v = v == null ? sorted.get(key) : v; if(oldValue.equals(v)) { segments[seg].put(key, newValue); return true; } else { return false; } } finally { segmentLocks[seg].unlock(stamp); } } @Override public int size() { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); // this is necessary to de-dupe keys return sorted.size(); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.subMap(fromKey, fromInclusive, toKey, toInclusive); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> subMap(K fromKey, K toKey) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.subMap(fromKey, toKey); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> tailMap(K fromKey) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.tailMap(fromKey); } finally { releaseSegmentLocks(stamps); } } @Override public ConcurrentNavigableMap<K, V> tailMap(K fromKey, boolean inclusive) { long[] stamps = grabAllSegmentWriteLocks(); try { sort(); return sorted.tailMap(fromKey, inclusive); } finally { releaseSegmentLocks(stamps); } } @Override public String toString() { long[] stamps = grabAllSegmentReadLocks(); try { sort(); return sorted.toString(); } finally { releaseSegmentLocks(stamps); } } @Override public Collection<V> values() { long[] stamps = grabAllSegmentReadLocks(); try { List<V> values = Lists.newArrayList(); values.addAll(sorted.values()); for (HashMap<K, V> segment : segments) { values.addAll(segment.values()); } return values; } finally { releaseSegmentLocks(stamps); } } /** * Return the segment where {@code key} should be initially written. * * @param key * @return the segment for {@code key} */ private int getSegment(Object key) { return Math.abs(key.hashCode() % 16); } /** * Method that is called by {@link #grabAllSegmentReadLock()} and * {@link #grabAllSegmentWriteLocks()} to do the work of grabbing the * appropriate lock. * * @param read * @return the stamps that represent all the locks that were grabbed. */ private long[] grabAllSegmentLocks(boolean read) { long[] stamps = new long[segmentLocks.length]; for (int i = 0; i < stamps.length; i++) { stamps[i] = read ? segmentLocks[i].readLock() : segmentLocks[i] .writeLock(); } return stamps; } /** * Grab the read locks for every segment * * @return the stamps for the read locks */ private long[] grabAllSegmentReadLocks() { return grabAllSegmentLocks(true); } /** * Grab the write lock for every segment. * * @return the stamps for the write locks */ private long[] grabAllSegmentWriteLocks() { return grabAllSegmentLocks(false); } /** * Release all the segment locks represented by the collection of * {@code stamps}. * * @param stamps */ private void releaseSegmentLocks(long[] stamps) { for (int i = 0; i < stamps.length; i++) { if(stamps[i] > 0) { segmentLocks[i].unlock(stamps[i]); } } } /** * Force an incremental sort. Take all the {@link #unsorted} items and place * them into the {@link #sorted} collection in the correct positions. This * method should only be called when the client is attempting to perform * some action in the {@link SortedMap} interface. */ private void sort() { for (HashMap<K, V> segment : segments) { sorted.putAll(segment); segment.clear(); } } }