diff --git a/src/dorkbox/util/collections/LockFreeBiMap.java b/src/dorkbox/util/collections/LockFreeBiMap.java index 199d323..b9cb1b3 100644 --- a/src/dorkbox/util/collections/LockFreeBiMap.java +++ b/src/dorkbox/util/collections/LockFreeBiMap.java @@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; * * This data structure is for many-read/few-write scenarios */ +@SuppressWarnings("WeakerAccess") public final class LockFreeBiMap { // Recommended for best performance while adhering to the "single writer principle". Must be static-final @@ -49,8 +50,9 @@ class LockFreeBiMap { HashMap.class, "reverseHashMap"); - private volatile HashMap forwardHashMap = new HashMap(); - private volatile HashMap reverseHashMap = new HashMap(); + private volatile HashMap forwardHashMap; + private volatile HashMap reverseHashMap; + private final LockFreeBiMap inverse; // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our @@ -58,6 +60,16 @@ class LockFreeBiMap { public LockFreeBiMap() { + forwardHashMap = new HashMap(); + reverseHashMap = new HashMap(); + this.inverse = new LockFreeBiMap(reverseHashMap, forwardHashMap, this); + } + + private + LockFreeBiMap(final HashMap forwardHashMap, final HashMap reverseHashMap, final LockFreeBiMap inverse) { + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.inverse = inverse; } /** @@ -70,6 +82,14 @@ class LockFreeBiMap { reverseHashMap.clear(); } + /** + * @return the inverse view of this bimap, which maps each of this bimap's values to its associated key. + */ + public + LockFreeBiMap inverse() { + return inverse; + } + /** * Replaces all of the mappings from the specified map to this bimap. * These mappings will replace any mappings that this map had for @@ -157,7 +177,12 @@ class LockFreeBiMap { K prevReverseValue = this.reverseHashMap.put(value, key); if (prevReverseValue != null) { // put the old value back - this.forwardHashMap.remove(key); + if (prevForwardValue != null) { + this.forwardHashMap.put(key, prevForwardValue); + } + else { + this.forwardHashMap.remove(key); + } this.reverseHashMap.put(value, prevReverseValue); throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!"); @@ -223,7 +248,7 @@ class LockFreeBiMap { throw new IllegalArgumentException("Key already exists. Keys and values must both be unique!"); } - if (this.reverseHashMap.containsValue(value)) { + if (this.reverseHashMap.containsKey(value)) { throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!"); } } @@ -275,36 +300,6 @@ class LockFreeBiMap { return value; } - /** - * Returns true if this map maps one or more keys to the - * specified value. - * - * @param key value whose presence in this map is to be tested - * - * @return true if this map maps one or more keys to the - * specified value - */ - public - boolean containsValue(final K key) { - // use the SWP to get a lock-free get of the value - return forwardREF.get(this).containsValue(key); - } - - /** - * Returns true if this map maps one or more keys to the - * specified value. - * - * @param value value whose presence in this map is to be tested - * - * @return true if this map maps one or more keys to the - * specified value - */ - public - boolean containsReverseValue(final V value) { - // use the SWP to get a lock-free get of the value - return reverseREF.get(this).containsValue(value); - } - /** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. diff --git a/src/dorkbox/util/collections/LockFreeHashMap.java b/src/dorkbox/util/collections/LockFreeHashMap.java index 460887d..e711f2f 100644 --- a/src/dorkbox/util/collections/LockFreeHashMap.java +++ b/src/dorkbox/util/collections/LockFreeHashMap.java @@ -16,11 +16,7 @@ package dorkbox.util.collections; import java.io.Serializable; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; /** @@ -102,20 +98,9 @@ class LockFreeHashMap implements Map, Cloneable, Serializable { this.hashMap = new HashMap(initialCapacity, loadFactor); } - - public synchronized - void replaceAll(final Map hashMap) { - if (hashMap == null) { - throw new NullPointerException("hashMap"); - } - - this.hashMap.clear(); - this.hashMap.putAll(hashMap); - } - @SuppressWarnings("unchecked") public - Map elements() { + Map getMap() { // use the SWP to get a lock-free get of the map. It's values are only valid at the moment this method is called. return Collections.unmodifiableMap(deviceREF.get(this)); } @@ -187,18 +172,18 @@ class LockFreeHashMap implements Map, Cloneable, Serializable { @Override public Set keySet() { - return elements().keySet(); + return getMap().keySet(); } @Override public Collection values() { - return elements().values(); + return getMap().values(); } @Override public Set> entrySet() { - return elements().entrySet(); + return getMap().entrySet(); } } diff --git a/src/dorkbox/util/collections/LockFreeIntBiMap.java b/src/dorkbox/util/collections/LockFreeIntBiMap.java new file mode 100644 index 0000000..acd8157 --- /dev/null +++ b/src/dorkbox/util/collections/LockFreeIntBiMap.java @@ -0,0 +1,440 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 dorkbox.util.collections; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import dorkbox.util.collections.IntMap.Entries; +import dorkbox.util.collections.IntMap.Keys; + +/** + * A bimap (or "bidirectional map") is a map that preserves the uniqueness of its values as well as that of its keys. This constraint + * enables bimaps to support an "inverse view", which is another bimap containing the same entries as this bimap but with reversed keys and values. + * + * This class uses the "single-writer-principle" for lock-free publication. + * + * Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by + * one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only + * one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free + * manner. + * + * According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for + * contended writes. + * + * This data structure is for many-read/few-write scenarios + */ +public +class LockFreeIntBiMap { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater forwardREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeIntBiMap.class, + IntMap.class, + "forwardHashMap"); + + private static final AtomicReferenceFieldUpdater reverseREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeIntBiMap.class, + ObjectIntMap.class, + "reverseHashMap"); + + private volatile IntMap forwardHashMap; + private volatile ObjectIntMap reverseHashMap; + + private final int defaultReturnValue; + private final LockFreeObjectIntBiMap inverse; + + // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this + // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our + // use-case 99% of the time) + + public static + void main(String[] args) { + LockFreeIntBiMap test = new LockFreeIntBiMap<>(); + String one = "One"; + String four = "Four"; + + test.put(1, one); + test.put(2, "Two"); + test.put(3, "Three"); + test.put(4, four); + // try { + // test.put(1, four); + // } catch (IllegalArgumentException e) { + // } + test.putForce(1, four); + test.put(5, one); + + System.out.println(test.toString()); + + System.out.println("Reverse"); + System.out.println(test.inverse().toString()); + } + + /** + * Creates a new bimap using @{link Integer#MIN_VALUE}. + */ + public + LockFreeIntBiMap() { + this(Integer.MIN_VALUE); + } + + /** + * The default return value is used for various get/put operations on the IntMap/ObjectIntMap. + * + * @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap. + */ + public + LockFreeIntBiMap(int defaultReturnValue) { + this(new IntMap(), new ObjectIntMap(), defaultReturnValue); + } + + /** + * The default return value is used for various get/put operations on the IntMap/ObjectIntMap. + * + * @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap. + */ + public + LockFreeIntBiMap(IntMap forwardHashMap, ObjectIntMap reverseHashMap, int defaultReturnValue) { + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.defaultReturnValue = defaultReturnValue; + + this.inverse = new LockFreeObjectIntBiMap(reverseHashMap, forwardHashMap, defaultReturnValue, this); + } + + LockFreeIntBiMap(final IntMap forwardHashMap, + final ObjectIntMap reverseHashMap, + final int defaultReturnValue, + final LockFreeObjectIntBiMap inverse) { + + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.defaultReturnValue = defaultReturnValue; + this.inverse = inverse; + } + + /** + * Removes all of the mappings from this bimap. + * The bimap will be empty after this call returns. + */ + public synchronized + void clear() { + forwardHashMap.clear(); + reverseHashMap.clear(); + } + + /** + * @return the inverse view of this bimap, which maps each of this bimap's values to its associated key. + */ + public + LockFreeObjectIntBiMap inverse() { + return inverse; + } + + /** + * Associates the specified value with the specified key in this bimap. + * If the bimap previously contained a mapping for the key, the old + * value is replaced. If the given value is already bound to a different + * key in this bimap, the bimap will remain unmodified. To avoid throwing + * an exception, call {@link #putForce(int, Object)} putForce(K, V) instead. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * + * @return the previous value associated with key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + * + * @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain + * unmodified in this event. To avoid this exception, call {@link #putForce(int, Object)} putForce(K, V) instead. + */ + public synchronized + V put(final int key, final V value) throws IllegalArgumentException { + V prevForwardValue = this.forwardHashMap.put(key, value); + if (prevForwardValue != null) { + reverseHashMap.remove(prevForwardValue, defaultReturnValue); + } + + int prevReverseValue = this.reverseHashMap.get(value, defaultReturnValue); + this.reverseHashMap.put(value, key); + if (prevReverseValue != defaultReturnValue) { + // put the old value back + if (prevForwardValue != null) { + this.forwardHashMap.put(key, prevForwardValue); + } + else { + this.forwardHashMap.remove(key); + } + + this.reverseHashMap.put(value, prevReverseValue); + + throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!"); + } + + return prevForwardValue; + } + + /** + * Associates the specified value with the specified key in this bimap. + * If the bimap previously contained a mapping for the key, the old + * value is replaced. This is an alternate form of {@link #put(int, Object)} + * that will silently ignore duplicates + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * + * @return the previous value associated with key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + public synchronized + V putForce(final int key, final V value) { + V prevForwardValue = this.forwardHashMap.put(key, value); + if (prevForwardValue != null) { + reverseHashMap.remove(prevForwardValue, defaultReturnValue); + } + + + int prevReverseValue = this.reverseHashMap.get(value, defaultReturnValue); + this.reverseHashMap.put(value, key); + + if (prevReverseValue != defaultReturnValue) { + forwardHashMap.remove(prevReverseValue); + } + + return prevForwardValue; + } + + /** + * Copies all of the mappings from the specified map to this map. + * These mappings will replace any mappings that this map had for + * any of the keys currently in the specified map. + * + * @param hashMap mappings to be stored in this map + * + * @throws NullPointerException if the specified map is null + * + * @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain + * unmodified in this event. To avoid this exception, call {@link #putAllForce(Map)} instead. + */ + public synchronized + void putAll(final Map hashMap) throws IllegalArgumentException { + LockFreeIntBiMap biMap = new LockFreeIntBiMap(); + + try { + for (Map.Entry entry : hashMap.entrySet()) { + Integer key = entry.getKey(); + V value = entry.getValue(); + + biMap.put(key, value); + + // we have to verify that the keys/values between the bimaps are unique + if (this.forwardHashMap.containsKey(key)) { + throw new IllegalArgumentException("Key already exists. Keys and values must both be unique!"); + } + + if (this.reverseHashMap.containsKey(value)) { + throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!"); + } + } + } catch (IllegalArgumentException e) { + // do nothing if there is an exception + throw e; + } + + // only if there are no problems with the creation of the new bimap AND the uniqueness constrain is guaranteed + this.forwardHashMap.putAll(biMap.forwardHashMap); + + // there is no putAll() method for ObjectIntMap + this.reverseHashMap.size = biMap.reverseHashMap.size; + this.reverseHashMap.keyTable = biMap.reverseHashMap.keyTable; + this.reverseHashMap.valueTable = biMap.reverseHashMap.valueTable; + this.reverseHashMap.capacity = biMap.reverseHashMap.capacity; + this.reverseHashMap.stashSize = biMap.reverseHashMap.stashSize; + } + + /** + * Copies all of the mappings from the specified map to this map. + * These mappings will replace any mappings that this map had for + * any of the keys currently in the specified map. This is an alternate + * form of {@link #putAll(Map)} putAll(K, V) that will silently + * ignore duplicates + * + * @param hashMap mappings to be stored in this map + * + * @throws NullPointerException if the specified map is null + */ + public synchronized + void putAllForce(final Map hashMap) { + for (Map.Entry entry : hashMap.entrySet()) { + Integer key = entry.getKey(); + V value = entry.getValue(); + + putForce(key, value); + } + } + + /** + * Removes the mapping for the specified key from this map if present. + * + * @param key key whose mapping is to be removed from the map + * + * @return the previous value associated with key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + public synchronized + V remove(final int key) { + V value = forwardHashMap.remove(key); + reverseHashMap.remove(value, defaultReturnValue); + return value; + } + + + /** + * Returns the value to which the specified key is mapped, + * or {@code null} if this map contains no mapping for the key. + *

+ *

More formally, if this map contains a mapping from a key + * {@code k} to a value {@code v} such that {@code (key==null ? k==null : + * key.equals(k))}, then this method returns {@code v}; otherwise + * it returns {@code null}. (There can be at most one such mapping.) + *

+ *

A return value of {@code null} does not necessarily + * indicate that the map contains no mapping for the key; it's also + * possible that the map explicitly maps the key to {@code null}. + * The {@link HashMap#containsKey containsKey} operation may be used to + * distinguish these two cases. + * + * @see #put(int, Object) + */ + @SuppressWarnings("unchecked") + public + V get(final int key) { + // use the SWP to get a lock-free get of the value + return (V) forwardREF.get(this).get(key); + } + + /** + * Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each + * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. + */ + @SuppressWarnings("unchecked") + public + Keys keys() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this) + .keys(); + } + + /** + * Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each + * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. + */ + @SuppressWarnings("unchecked") + public + Iterator values() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this).values(); + } + + /** + * Returns true if this bimap contains no key-value mappings. + * + * @return true if this bimap contains no key-value mappings + */ + public + boolean isEmpty() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this) + .size == 0; + } + + /** + * Returns the number of key-value mappings in this map. If the + * map contains more than Integer.MAX_VALUE elements, returns + * Integer.MAX_VALUE. + * + * @return the number of key-value mappings in this map + */ + public + int size() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this) + .size; + } + + @Override + public + boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final LockFreeIntBiMap that = (LockFreeIntBiMap) o; + + if (defaultReturnValue != that.defaultReturnValue) { + return false; + } + if (!forwardHashMap.equals(that.forwardHashMap)) { + return false; + } + return reverseHashMap.equals(that.reverseHashMap); + } + + @Override + public + int hashCode() { + int result = forwardHashMap.hashCode(); + result = 31 * result + reverseHashMap.hashCode(); + result = 31 * result + defaultReturnValue; + return result; + } + + @Override + public + String toString() { + StringBuilder builder = new StringBuilder("LockFreeIntBiMap {"); + + Keys keys = keys(); + Iterator values = values(); + + while (keys.hasNext) { + builder.append(keys.next()); + builder.append(" (") + .append(values.next()) + .append("), "); + } + + int length = builder.length(); + if (length > 1) { + // delete the ', ' + builder.delete(length - 2, length); + } + + builder.append('}'); + + return builder.toString(); + } +} diff --git a/src/dorkbox/util/collections/LockFreeIntMap.java b/src/dorkbox/util/collections/LockFreeIntMap.java new file mode 100644 index 0000000..63be087 --- /dev/null +++ b/src/dorkbox/util/collections/LockFreeIntMap.java @@ -0,0 +1,177 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 dorkbox.util.collections; + +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import dorkbox.util.collections.IntMap.Keys; +import dorkbox.util.collections.IntMap.Values; + +/** + * This class uses the "single-writer-principle" for lock-free publication. + *

+ * Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by + * one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only + * one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free + * manner. + *

+ * According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for + * contended writes. + *

+ * This data structure is for many-read/few-write scenarios + * + * This is an unordered map that uses int keys. This implementation is a cuckoo hash map using 3 hashes, random walking, and a small stash + * for problematic keys. Null values are allowed. No allocation is done except when growing the table size.
+ *
+ * This map performs very fast get, containsKey, and remove (typically O(1), worst case O(log(n))). Put may be a bit slower, + * depending on hash collisions. Load factors greater than 0.91 greatly increase the chances the map will have to rehash to the + * next higher POT size. + * @author Nathan Sweet + */ +@SuppressWarnings("unchecked") +public final +class LockFreeIntMap implements Cloneable, Serializable { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater deviceREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeIntMap.class, + IntMap.class, + "map"); + + private volatile IntMap map; + + // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this + // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our + // use-case 99% of the time) + + /** + * Constructs an empty IntMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + public + LockFreeIntMap() { + map = new IntMap(); + } + + /** + * Constructs an empty IntMap with the specified initial + * capacity and the default load factor (0.75). + * + * @param initialCapacity the initial capacity. + * + * @throws IllegalArgumentException if the initial capacity is negative. + */ + public + LockFreeIntMap(int initialCapacity) { + map = new IntMap(initialCapacity); + } + + /** + * Constructs an empty IntMap with the specified initial + * capacity and load factor. + * + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + * + * @throws IllegalArgumentException if the initial capacity is negative + * or the load factor is nonpositive + */ + public + LockFreeIntMap(int initialCapacity, float loadFactor) { + this.map = new IntMap(initialCapacity, loadFactor); + } + + + public + int size() { + // use the SWP to get a lock-free get of the value + return deviceREF.get(this) + .size; + } + + public + boolean isEmpty() { + // use the SWP to get a lock-free get of the value + return deviceREF.get(this) + .size == 0; + } + + public + boolean containsKey(final int key) { + // use the SWP to get a lock-free get of the value + return deviceREF.get(this) + .containsKey(key); + } + + /** + * Returns true if the specified value is in the map. Note this traverses the entire map and compares every value, which may be + * an expensive operation. + * + * @param identity If true, uses == to compare the specified value with values in the map. If false, uses + * {@link #equals(Object)}. + */ + public + boolean containsValue(final Object value, boolean identity) { + // use the SWP to get a lock-free get of the value + return deviceREF.get(this) + .containsValue(value, identity); + } + + public + V get(final int key) { + // use the SWP to get a lock-free get of the value + return (V) deviceREF.get(this) + .get(key); + } + + public synchronized + V put(final int key, final V value) { + return map.put(key, value); + } + + public synchronized + V remove(final int key) { + return map.remove(key); + } + + public synchronized + void putAll(final IntMap map) { + this.map.putAll(map); + } + + public synchronized + void clear() { + map.clear(); + } + + /** + * DO NOT MODIFY THE MAP VIA THIS! It will result in unknown object visibility! + */ + public + Keys keySet() { + return deviceREF.get(this) + .keys(); + } + + /** + * DO NOT MODIFY THE MAP VIA THIS! It will result in unknown object visibility! + */ + public + Values values() { + return deviceREF.get(this) + .values(); + } +} diff --git a/src/dorkbox/util/collections/LockFreeObjectIntBiMap.java b/src/dorkbox/util/collections/LockFreeObjectIntBiMap.java new file mode 100644 index 0000000..304afd0 --- /dev/null +++ b/src/dorkbox/util/collections/LockFreeObjectIntBiMap.java @@ -0,0 +1,417 @@ +/* + * Copyright 2018 dorkbox, llc. + * + * 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 dorkbox.util.collections; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import dorkbox.util.collections.IntMap.Entries; +import dorkbox.util.collections.IntMap.Keys; + +/** + * A bimap (or "bidirectional map") is a map that preserves the uniqueness of its values as well as that of its keys. This constraint + * enables bimaps to support an "inverse view", which is another bimap containing the same entries as this bimap but with reversed keys and values. + * + * This class uses the "single-writer-principle" for lock-free publication. + * + * Since there are only 2 methods to guarantee that modifications can only be called one-at-a-time (either it is only called by + * one thread, or only one thread can access it at a time) -- we chose the 2nd option -- and use 'synchronized' to make sure that only + * one thread can access this modification methods at a time. Getting or checking the presence of values can then happen in a lock-free + * manner. + * + * According to my benchmarks, this is approximately 25% faster than ConcurrentHashMap for (all types of) reads, and a lot slower for + * contended writes. + * + * This data structure is for many-read/few-write scenarios + */ +public +class LockFreeObjectIntBiMap { + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private static final AtomicReferenceFieldUpdater forwardREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeObjectIntBiMap.class, + ObjectIntMap.class, + "forwardHashMap"); + + private static final AtomicReferenceFieldUpdater reverseREF = + AtomicReferenceFieldUpdater.newUpdater(LockFreeObjectIntBiMap.class, + IntMap.class, + "reverseHashMap"); + + private volatile ObjectIntMap forwardHashMap; + private volatile IntMap reverseHashMap; + + private final int defaultReturnValue; + private final LockFreeIntBiMap inverse; + + // synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this + // section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our + // use-case 99% of the time) + + /** + * Creates a new bimap using @{link Integer#MIN_VALUE}. + */ + public + LockFreeObjectIntBiMap() { + this(Integer.MIN_VALUE); + } + + /** + * The default return value is used for various get/put operations on the IntMap/ObjectIntMap. + * + * @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap. + */ + public + LockFreeObjectIntBiMap(int defaultReturnValue) { + this(new ObjectIntMap(), new IntMap(), defaultReturnValue); + } + + /** + * The default return value is used for various get/put operations on the IntMap/ObjectIntMap. + * + * @param defaultReturnValue value used for various get/put operations on the IntMap/ObjectIntMap. + */ + LockFreeObjectIntBiMap(ObjectIntMap forwardHashMap, IntMap reverseHashMap, int defaultReturnValue) { + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.defaultReturnValue = defaultReturnValue; + + this.inverse = new LockFreeIntBiMap(reverseHashMap, forwardHashMap, defaultReturnValue, this); + } + + LockFreeObjectIntBiMap(final ObjectIntMap forwardHashMap, + final IntMap reverseHashMap, + final int defaultReturnValue, + final LockFreeIntBiMap inverse) { + + this.forwardHashMap = forwardHashMap; + this.reverseHashMap = reverseHashMap; + this.defaultReturnValue = defaultReturnValue; + this.inverse = inverse; + } + + /** + * Removes all of the mappings from this bimap. + * + * The bimap will be empty after this call returns. + */ + public synchronized + void clear() { + forwardHashMap.clear(); + reverseHashMap.clear(); + } + + /** + * @return the inverse view of this bimap, which maps each of this bimap's values to its associated key. + */ + public + LockFreeIntBiMap inverse() { + return inverse; + } + + /** + * Associates the specified value with the specified key in this bimap. + * If the bimap previously contained a mapping for the key, the old + * value is replaced. If the given value is already bound to a different + * key in this bimap, the bimap will remain unmodified. To avoid throwing + * an exception, call {@link #putForce(Object, int)} instead. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * + * @return the previous value associated with key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + * + * @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain + * unmodified in this event. To avoid this exception, call {@link #putForce(Object, int)} instead. + */ + public synchronized + int put(final V key, final int value) throws IllegalArgumentException { + int prevForwardValue = this.forwardHashMap.get(key, defaultReturnValue); + this.forwardHashMap.put(key, value); + if (prevForwardValue != defaultReturnValue) { + reverseHashMap.remove(prevForwardValue); + } + + V prevReverseValue = this.reverseHashMap.put(value, key); + if (prevReverseValue != null) { + // put the old value back + if (prevForwardValue != defaultReturnValue) { + this.forwardHashMap.put(key, prevForwardValue); + } + else { + this.forwardHashMap.remove(key, defaultReturnValue); + } + + this.reverseHashMap.put(value, prevReverseValue); + + throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!"); + } + + return prevForwardValue; + } + + /** + * Associates the specified value with the specified key in this bimap. + * If the bimap previously contained a mapping for the key, the old + * value is replaced. This is an alternate form of {@link #put(Object, int)} + * that will silently ignore duplicates + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * + * @return the previous value associated with key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key.) + */ + public synchronized + int putForce(final V key, final int value) { + int prevForwardValue = this.forwardHashMap.get(key, defaultReturnValue); + this.forwardHashMap.put(key, value); + if (prevForwardValue != defaultReturnValue) { + reverseHashMap.remove(prevForwardValue); + } + + + V prevReverseValue = this.reverseHashMap.get(value); + this.reverseHashMap.put(value, key); + + if (prevReverseValue != null) { + forwardHashMap.remove(prevReverseValue, defaultReturnValue); + } + + return prevForwardValue; + } + + /** + * Copies all of the mappings from the specified map to this map. + * These mappings will replace any mappings that this map had for + * any of the keys currently in the specified map. + * + * @param hashMap mappings to be stored in this map + * + * @throws NullPointerException if the specified map is null + * + * @throws IllegalArgumentException if the given value is already bound to a different key in this bimap. The bimap will remain + * unmodified in this event. To avoid this exception, call {@link #putAllForce(Map)} instead. + */ + public synchronized + void putAll(final Map hashMap) throws IllegalArgumentException { + LockFreeObjectIntBiMap biMap = new LockFreeObjectIntBiMap(); + + try { + for (Map.Entry entry : hashMap.entrySet()) { + V key = entry.getKey(); + Integer value = entry.getValue(); + + biMap.put(key, value); + + // we have to verify that the keys/values between the bimaps are unique + if (this.forwardHashMap.containsKey(key)) { + throw new IllegalArgumentException("Key already exists. Keys and values must both be unique!"); + } + + if (this.reverseHashMap.containsKey(value)) { + throw new IllegalArgumentException("Value already exists. Keys and values must both be unique!"); + } + } + } catch (IllegalArgumentException e) { + // do nothing if there is an exception + throw e; + } + + // there is no putAll() method for ObjectIntMap + this.forwardHashMap.size = biMap.forwardHashMap.size; + this.forwardHashMap.keyTable = biMap.forwardHashMap.keyTable; + this.forwardHashMap.valueTable = biMap.forwardHashMap.valueTable; + this.forwardHashMap.capacity = biMap.forwardHashMap.capacity; + this.forwardHashMap.stashSize = biMap.forwardHashMap.stashSize; + + // only if there are no problems with the creation of the new bimap AND the uniqueness constrain is guaranteed + this.reverseHashMap.putAll(biMap.reverseHashMap); + } + + /** + * Copies all of the mappings from the specified map to this map. + * These mappings will replace any mappings that this map had for + * any of the keys currently in the specified map. This is an alternate + * form of {@link #putAll(Map)} putAll(K, V) that will silently + * ignore duplicates + * + * @param hashMap mappings to be stored in this map + * + * @throws NullPointerException if the specified map is null + */ + public synchronized + void putAllForce(final Map hashMap) { + for (Map.Entry entry : hashMap.entrySet()) { + V key = entry.getKey(); + Integer value = entry.getValue(); + + putForce(key, value); + } + } + + /** + * Removes the mapping for the specified key from this map if present. + * + * @param key key whose mapping is to be removed from the map + * + * @return the previous value associated with key, or + * defaultReturnValue if there was no mapping for key. + * (A defaultReturnValue return can also indicate that the map + * previously associated defaultReturnValue with key.) + */ + public synchronized + int remove(final V key) { + int value = forwardHashMap.remove(key, defaultReturnValue); + reverseHashMap.remove(value); + return value; + } + + + /** + * Returns the value to which the specified key is mapped, + * or {@code defaultReturnValue} if this map contains no mapping for the key. + *

+ *

More formally, if this map contains a mapping from a key + * {@code k} to a value {@code v} such that {@code (key==null ? k==null : + * key.equals(k))}, then this method returns {@code v}; otherwise + * it returns {@code defaultReturnValue}. (There can be at most one such mapping.) + *

+ *

A return value of {@code defaultReturnValue} does not necessarily + * indicate that the map contains no mapping for the key; it's also + * possible that the map explicitly maps the key to {@code null}. + * The {@link HashMap#containsKey containsKey} operation may be used to + * distinguish these two cases. + * + * @see #put(Object, int) + */ + @SuppressWarnings("unchecked") + public + int get(final V key) { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this).get(key, defaultReturnValue); + } + + /** + * Returns an iterator for the keys in the map. Remove is supported. Note that the same iterator instance is returned each + * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. + */ + @SuppressWarnings("unchecked") + public + Iterator keys() { + // the ObjectIntMap doesn't have iterators, but the IntMap does + return inverse.values(); + } + + /** + * Returns an iterator for the values in the map. Remove is supported. Note that the same iterator instance is returned each + * time this method is called. Use the {@link Entries} constructor for nested or multithreaded iteration. + */ + @SuppressWarnings("unchecked") + public + Keys values() { + // the ObjectIntMap doesn't have iterators, but the IntMap does + return inverse.keys(); + } + + /** + * Returns true if this bimap contains no key-value mappings. + * + * @return true if this bimap contains no key-value mappings + */ + public + boolean isEmpty() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this) + .size == 0; + } + + /** + * Returns the number of key-value mappings in this map. If the + * map contains more than Integer.MAX_VALUE elements, returns + * Integer.MAX_VALUE. + * + * @return the number of key-value mappings in this map + */ + public + int size() { + // use the SWP to get a lock-free get of the value + return forwardREF.get(this) + .size; + } + + @Override + public + boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final LockFreeObjectIntBiMap that = (LockFreeObjectIntBiMap) o; + + if (defaultReturnValue != that.defaultReturnValue) { + return false; + } + if (!forwardHashMap.equals(that.forwardHashMap)) { + return false; + } + return reverseHashMap.equals(that.reverseHashMap); + } + + @Override + public + int hashCode() { + int result = forwardHashMap.hashCode(); + result = 31 * result + reverseHashMap.hashCode(); + result = 31 * result + defaultReturnValue; + return result; + } + + @Override + public + String toString() { + StringBuilder builder = new StringBuilder("LockFreeObjectIntBiMap {"); + + Iterator keys = keys(); + Keys values = values(); + + while (keys.hasNext()) { + builder.append(keys.next()); + builder.append(" (") + .append(values.next()) + .append("), "); + } + + int length = builder.length(); + if (length > 1) { + // delete the ', ' + builder.delete(length - 2, length); + } + + builder.append('}'); + + return builder.toString(); + } +}