diff --git a/src/dorkbox/collections/LockFreeIntStringMap.java b/src/dorkbox/collections/LockFreeIntStringMap.java deleted file mode 100644 index b226698..0000000 --- a/src/dorkbox/collections/LockFreeIntStringMap.java +++ /dev/null @@ -1,225 +0,0 @@ -package dorkbox.collections; - -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -/** - * 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. - */ -@SuppressWarnings("unchecked") -public -class LockFreeIntStringMap { - public static final String version = Collections.version; - - private static final AtomicReferenceFieldUpdater mapREF = AtomicReferenceFieldUpdater.newUpdater( - LockFreeIntStringMap.class, - IntMap.class, - "map"); - - private volatile IntMap map; - - - public LockFreeIntStringMap() { - this.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 - LockFreeIntStringMap(int initialCapacity) { - map = new IntMap(initialCapacity); - } - - /** - * Constructs an empty IntMap with the specified initial - * capacity and the default load factor (0.75). - * - * @throws IllegalArgumentException if the initial capacity is negative. - */ - public - LockFreeIntStringMap(LockFreeIntStringMap map) { - this.map = new IntMap(map.map); - } - - /** - * 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 - LockFreeIntStringMap(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 mapREF.get(this) - .size; - } - - public - boolean isEmpty() { - // use the SWP to get a lock-free get of the value - return mapREF.get(this) - .size == 0; - } - - public - boolean containsKey(final int key) { - // use the SWP to get a lock-free get of the value - return mapREF.get(this) - .containsKey(key); - } - - public - boolean containsKey(final String key) { - // use the SWP to get a lock-free get of the value - return mapREF.get(this) - .containsKey(key.hashCode()); - } - - /** - * 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 mapREF.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) mapREF.get(this) - .get(key); - } - - public - V get(final String key) { - // use the SWP to get a lock-free get of the value - return (V) mapREF.get(this) - .get(key.hashCode()); - } - - public synchronized - V put(final int key, final V value) { - return map.put(key, value); - } - - public synchronized - V put(final String key, final V value) { - return map.put(key.hashCode(), value); - } - - public synchronized - V remove(final int key) { - return map.remove(key); - } - - public synchronized - V remove(final String key) { - return map.remove(key.hashCode()); - } - - public synchronized - void putAll(final IntMap map) { - this.map.putAll(map); - } - - /** - * DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility! - * - * 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 IntMap.Entries} constructor for nested or multi-threaded iteration. - */ - public - IntMap.Keys keys() { - return mapREF.get(this) - .keys(); - } - - /** - * DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility! - * - * 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 IntMap.Entries} constructor for nested or multi-threaded iteration. - */ - public - IntMap.Values values() { - return mapREF.get(this) - .values(); - } - - /** - * DO NOT MODIFY THE MAP VIA THIS (unless you synchronize around it!) It will result in unknown object visibility! - * - * Returns an iterator for the entries in the map. Remove is supported. Note that the same iterator instance is returned each - * time this method is called. Use the {@link IntMap.Entries} constructor for nested or multi-threaded iteration. - */ - public - IntMap.Entries entries() { - return mapREF.get(this) - .entries(); - } - - public synchronized - void clear() { - map.clear(); - } - - /** - * Identity equals only! - */ - @Override - public - boolean equals(final Object o) { - return this == o; - } - - @Override - public - int hashCode() { - return mapREF.get(this) - .hashCode(); - } - - @Override - public - String toString() { - return mapREF.get(this) - .toString(); - } -} diff --git a/src/dorkbox/collections/LockFreeIntStringMap.kt b/src/dorkbox/collections/LockFreeIntStringMap.kt new file mode 100644 index 0000000..d05b434 --- /dev/null +++ b/src/dorkbox/collections/LockFreeIntStringMap.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2023 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.collections + +import java.io.Serializable +import java.util.concurrent.atomic.* + +/** + * 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 + */ +class LockFreeIntStringMap : MutableMap, Cloneable, Serializable { + + @Volatile + private var hashMap: IntMap + + + // 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 HashMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + constructor() { + hashMap = IntMap() + } + + /** + * Constructs an empty HashMap 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. + */ + constructor(initialCapacity: Int) { + hashMap = IntMap(initialCapacity) + } + + /** + * Constructs a new HashMap with the same mappings as the + * specified Map. The HashMap is created with + * default load factor (0.75) and an initial capacity sufficient to + * hold the mappings in the specified Map. + * + * @param map the map whose mappings are to be placed in this map + * + * @throws NullPointerException if the specified map is null + */ + constructor(map: Map) { + hashMap = IntMap(map.size) + map.forEach { (index, key) -> + hashMap.put(index, key) + } + } + + constructor(map: LockFreeIntStringMap) { + hashMap = IntMap(map.hashMap) + } + + /** + * Constructs an empty HashMap 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 + */ + constructor(initialCapacity: Int, loadFactor: Float) { + hashMap = IntMap(initialCapacity, loadFactor) + } + + val map: IntMap + get() { + @Suppress("UNCHECKED_CAST") + return mapREF[this] as IntMap + } + + + override val size: Int + get() { + // use the SWP to get a lock-free get of the value + return mapREF[this].size + } + + override val keys: MutableSet + get() { + return map.keys() + } + + override val values: MutableCollection + get() { + @Suppress("UNCHECKED_CAST") + return map.values() as MutableCollection + } + + override val entries: MutableSet> + get() { + @Suppress("UNCHECKED_CAST") + return map.entries() as MutableSet> + } + + override fun isEmpty(): Boolean { + // use the SWP to get a lock-free get of the value + return mapREF[this].isEmpty() + } + + override fun containsKey(key: Int): Boolean { + // use the SWP to get a lock-free get of the value + return mapREF[this].containsKey(key) + } + + override fun containsValue(value: String?): Boolean { + // use the SWP to get a lock-free get of the value + return mapREF[this].containsValue(value) + } + + override operator fun get(key: Int): String? { + return mapREF[this][key] as String? + } + + @Synchronized + override fun put(key: Int, value: String?): String? { + return hashMap.put(key, value) + } + + @Synchronized + override fun putAll(from: Map) { + hashMap.putAll(from) + } + + @Synchronized + override fun remove(key: Int): String? { + return hashMap.remove(key) + } + + @Synchronized + fun removeAllValues(value: String) { + val iterator = hashMap.entries().iterator() + while (iterator.hasNext()) { + val value1 = iterator.next() + if (value1.value == value) { + iterator.remove() + } + } + } + + @Synchronized + fun replaceAll(hashMap: IntMap) { + this.hashMap.clear() + this.hashMap.putAll(hashMap) + } + + @Synchronized + override fun clear() { + hashMap.clear() + } + + override fun equals(other: Any?): Boolean { + return (mapREF[this] == other) + } + + override fun hashCode(): Int { + return mapREF[this].hashCode() + } + + override fun toString(): String { + return mapREF[this].toString() + } + + // this must be at the end of the file! + companion object { + const val version = Collections.version + + // Recommended for best performance while adhering to the "single writer principle". Must be static-final + private val mapREF = AtomicReferenceFieldUpdater.newUpdater( + LockFreeIntStringMap::class.java, IntMap::class.java, "hashMap" + ) + } +}