space.vectrix.flare.fastutil.Long2ObjectSyncMapImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of flare-fastutil Show documentation
Show all versions of flare-fastutil Show documentation
Useful thread-safe collections with performance in mind.
/*
* This file is part of flare, licensed under the MIT License (MIT).
*
* Copyright (c) vectrix.space
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package space.vectrix.flare.fastutil;
import static java.util.Objects.requireNonNull;
import it.unimi.dsi.fastutil.longs.AbstractLong2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.objects.AbstractObjectSet;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.AbstractMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.LongFunction;
import java.util.function.IntFunction;
/* package */ final class Long2ObjectSyncMapImpl extends AbstractLong2ObjectMap implements Long2ObjectSyncMap {
private static final long serialVersionUID = 1;
/**
* A single implicit lock when dealing with {@code dirty} mutations.
*/
private transient final Object lock = new Object();
/**
* The read only map that does not require a lock and does not allow mutations.
*/
private transient volatile Long2ObjectMap> read;
/**
* Represents whether the {@code dirty} map has changes the {@code read} map
* does not have yet.
*/
private transient volatile boolean amended;
/**
* The read/write map that requires a lock and allows mutations.
*/
private transient Long2ObjectMap> dirty;
/**
* Represents the amount of times an attempt has been made to access the
* {@code dirty} map while {@code amended} is {@code true}.
*/
private transient int misses;
private transient EntrySetView entrySet;
private final IntFunction>> function;
private final float promotionFactor;
/* package */ Long2ObjectSyncMapImpl(final @NonNull IntFunction>> function, final int initialCapacity) {
this(function, initialCapacity, 1.0F);
}
/* package */ Long2ObjectSyncMapImpl(final @NonNull IntFunction>> function, final int initialCapacity, final float promotionFactor) {
if(promotionFactor <= 0.0F || promotionFactor > 1.0F) throw new IllegalArgumentException("Promotion factor must be more than 0 and less than or equal to 1");
this.function = function;
this.promotionFactor = promotionFactor;
this.read = function.apply(initialCapacity);
}
// Query Operations
@Override
public int size() {
this.promoteIfNeeded();
int size = 0;
for(final ExpungingValue value : this.read.values()) {
if(value.exists()) size++;
}
return size;
}
@Override
public boolean isEmpty() {
this.promoteIfNeeded();
for(final ExpungingValue value : this.read.values()) {
if(value.exists()) return false;
}
return true;
}
@Override
public boolean containsValue(final @Nullable Object value) {
for(final Long2ObjectMap.Entry entry : this.long2ObjectEntrySet()) {
if(Objects.equals(entry.getValue(), value)) return true;
}
return false;
}
@Override
public boolean containsKey(final long key) {
ExpungingValue entry;
if((entry = this.read.get(key)) == null && this.amended) {
synchronized(this.lock) {
if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
entry = this.dirty.get(key);
// The slow path should be avoided, even if the value does
// not match or is present. So we mark a miss, to eventually
// promote and take a faster path.
this.missLocked();
}
}
}
return entry != null && entry.exists();
}
@Override
public @Nullable V get(final long key) {
ExpungingValue entry;
if((entry = this.read.get(key)) == null && this.amended) {
synchronized(this.lock) {
if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
entry = this.dirty.get(key);
// The slow path should be avoided, even if the value does
// not match or is present. So we mark a miss, to eventually
// promote and take a faster path.
this.missLocked();
}
}
}
return entry != null ? entry.get() : null;
}
@Override
@SuppressWarnings("ConstantConditions")
public @Nullable V computeIfAbsent(final long key, final @NonNull LongFunction extends V> mappingFunction) {
requireNonNull(mappingFunction, "mappingFunction");
ExpungingValue entry; V current;
if((entry = this.read.get(key)) != null) {
if((current = entry.get()) == null) current = entry.set(mappingFunction.apply(key));
return current;
}
synchronized(this.lock) {
if((entry = this.read.get(key)) != null) {
// The entry was previously expunged, which implies this entry
// is not within the dirty map.
if(entry.tryUnexpunge()) {
this.dirty.put(key, entry);
}
if((current = entry.get()) == null) current = entry.set(mappingFunction.apply(key));
} else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
if((current = entry.get()) == null) current = entry.set(mappingFunction.apply(key));
this.missLocked();
} else {
if(!this.amended) {
// Adds the first new key to the dirty map and marks it as
// amended.
this.dirtyLocked();
this.amended = true;
}
this.dirty.put(key, new ExpungingValueImpl<>(current = mappingFunction.apply(key)));
}
}
return current;
}
@Override
@SuppressWarnings("ConstantConditions")
public @Nullable V computeIfAbsent(final long key, final @NonNull Long2ObjectFunction extends V> mappingFunction) {
requireNonNull(mappingFunction, "mappingFunction");
ExpungingValue entry; V current;
if((entry = this.read.get(key)) != null) {
if((current = entry.get()) == null) current = entry.set(mappingFunction.get(key));
return current;
}
synchronized(this.lock) {
if((entry = this.read.get(key)) != null) {
// The entry was previously expunged, which implies this entry
// is not within the dirty map.
if(entry.tryUnexpunge()) {
this.dirty.put(key, entry);
}
if((current = entry.get()) == null) current = entry.set(mappingFunction.get(key));
} else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
if((current = entry.get()) == null) current = entry.set(mappingFunction.get(key));
this.missLocked();
} else {
if(!this.amended) {
// Adds the first new key to the dirty map and marks it as
// amended.
this.dirtyLocked();
this.amended = true;
}
this.dirty.put(key, new ExpungingValueImpl<>(current = mappingFunction.get(key)));
}
}
return current;
}
@Override
public @Nullable V computeIfPresent(final long key, final @NonNull BiFunction super Long, ? super V, ? extends V> remappingFunction) {
requireNonNull(remappingFunction, "remappingFunction");
ExpungingValue entry; V current;
if((entry = this.read.get(key)) != null) {
if((current = entry.get()) != null) entry.trySet(current = remappingFunction.apply(key, current));
return current;
}
synchronized(this.lock) {
if((entry = this.read.get(key)) != null) {
if((current = entry.get()) != null) entry.trySet(current = remappingFunction.apply(key, current));
} else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
if((current = entry.get()) != null) entry.trySet(current = remappingFunction.apply(key, current));
this.missLocked();
} else {
current = null;
}
}
return current;
}
@Override
@SuppressWarnings("ConstantConditions")
public @Nullable V compute(final long key, final @NonNull BiFunction super Long, ? super V, ? extends V> remappingFunction) {
requireNonNull(remappingFunction, "remappingFunction");
ExpungingValue entry; final V current;
if((entry = this.read.get(key)) != null) {
current = entry.set(remappingFunction.apply(key, entry.get()));
return current;
}
synchronized(this.lock) {
if((entry = this.read.get(key)) != null) {
// The entry was previously expunged, which implies this entry
// is not within the dirty map.
if(entry.tryUnexpunge()) {
this.dirty.put(key, entry);
}
current = entry.set(remappingFunction.apply(key, entry.get()));
} else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
current = entry.set(remappingFunction.apply(key, entry.get()));
this.missLocked();
} else {
if(!this.amended) {
// Adds the first new key to the dirty map and marks it as
// amended.
this.dirtyLocked();
this.amended = true;
}
this.dirty.put(key, new ExpungingValueImpl<>(current = remappingFunction.apply(key, null)));
}
}
return current;
}
@Override
@SuppressWarnings("ConstantConditions")
public @Nullable V putIfAbsent(final long key, final @NonNull V value) {
requireNonNull(value, "value");
ExpungingValue entry; Map.Entry result;
if((entry = this.read.get(key)) != null) {
if((result = entry.putIfAbsent(value)).getKey() == Boolean.TRUE) {
return result.getValue();
}
}
synchronized(this.lock) {
if((entry = this.read.get(key)) != null) {
// The entry was previously expunged, which implies this entry
// is not within the dirty map.
if(entry.tryUnexpunge()) {
this.dirty.put(key, entry);
}
result = entry.putIfAbsent(value);
} else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
result = entry.putIfAbsent(value);
this.missLocked();
} else {
if(!this.amended) {
// Adds the first new key to the dirty map and marks it as
// amended.
this.dirtyLocked();
this.amended = true;
}
this.dirty.put(key, new ExpungingValueImpl<>(value));
result = new AbstractMap.SimpleImmutableEntry<>(Boolean.TRUE, null);
}
}
return result.getValue();
}
@Override
public @Nullable V put(final long key, final @NonNull V value) {
return this.putValue(key, value, false);
}
@SuppressWarnings("ConstantConditions")
private V putValue(final long key, final @NonNull V value, final boolean onlyIfPresent) {
requireNonNull(value, "value");
ExpungingValue entry = this.read.get(key);
V previous = entry != null ? entry.get() : null;
if(entry != null && entry.trySet(value)) return previous;
synchronized(this.lock) {
if((entry = this.read.get(key)) != null) {
previous = entry.get();
// If entry can be absent and previously expunged, add the
// entry back to the dirty map.
if(onlyIfPresent) {
entry.trySet(value);
} else if(entry.tryUnexpungeAndSet(value)) {
this.dirty.put(key, entry);
} else {
entry.set(value);
}
} else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
previous = entry.get();
entry.set(value);
this.missLocked();
} else if(!onlyIfPresent) {
if(!this.amended) {
// Adds the first new key to the dirty map and marks it as
// amended.
this.dirtyLocked();
this.amended = true;
}
this.dirty.put(key, new ExpungingValueImpl<>(value));
}
}
return previous;
}
@Override
public @Nullable V remove(final long key) {
ExpungingValue entry;
if((entry = this.read.get(key)) == null && this.amended) {
synchronized(this.lock) {
if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
entry = this.dirty.remove(key);
// The slow path should be avoided, even if the value does
// not match or is present. So we mark a miss, to eventually
// promote and take a faster path.
this.missLocked();
}
}
}
return entry != null ? entry.clear() : null;
}
@Override
@SuppressWarnings("AssignmentUsedAsCondition")
public boolean remove(final long key, final @NonNull Object value) {
requireNonNull(value, "value");
ExpungingValue entry;
if((entry = this.read.get(key)) == null && this.amended) {
synchronized(this.lock) {
if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
final boolean present;
if(present = (((entry = this.dirty.get(key))) != null && entry.replace(value, null))) {
this.dirty.remove(key);
}
// The slow path should be avoided, even if the value does
// not match or is present. So we mark a miss, to eventually
// promote and take a faster path.
this.missLocked();
return present;
}
}
}
return entry != null && entry.replace(value, null);
}
@Override
public @Nullable V replace(final long key, final @NonNull V value) {
return this.putValue(key, value, true);
}
@Override
public boolean replace(final long key, final @NonNull V currentValue, final @NonNull V newValue) {
requireNonNull(currentValue, "currentValue");
requireNonNull(newValue, "newValue");
ExpungingValue entry;
if((entry = this.read.get(key)) == null && this.amended) {
synchronized(this.lock) {
if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
final boolean present = ((entry = this.dirty.get(key)) != null && entry.replace(currentValue, newValue));
// The slow path should be avoided, even if the value does
// not match or is present. So we mark a miss, to eventually
// promote and take a faster path.
this.missLocked();
return present;
}
}
}
return entry != null && entry.replace(currentValue, newValue);
}
// Bulk Operations
@Override
public void forEach(final @NonNull BiConsumer super Long, ? super V> action) {
requireNonNull(action, "action");
this.promoteIfNeeded();
V value;
for(final Long2ObjectMap.Entry> that : this.read.long2ObjectEntrySet()) {
if((value = that.getValue().get()) != null) {
action.accept(that.getLongKey(), value);
}
}
}
@Override
public void putAll(final @NonNull Map extends Long, ? extends V> map) {
requireNonNull(map, "map");
for(final Map.Entry extends Long, ? extends V> entry : map.entrySet()) {
this.putValue(entry.getKey(), entry.getValue(), false);
}
}
@Override
public void replaceAll(final @NonNull BiFunction super Long, ? super V, ? extends V> function) {
requireNonNull(function, "function");
this.promoteIfNeeded();
ExpungingValue entry; V value;
for(final Long2ObjectMap.Entry> that : this.read.long2ObjectEntrySet()) {
if((value = (entry = that.getValue()).get()) != null) {
entry.trySet(function.apply(that.getLongKey(), value));
}
}
}
@Override
public void clear() {
synchronized(this.lock) {
this.read = this.function.apply(this.read.size());
this.amended = false;
this.dirty = null;
this.misses = 0;
}
}
// Views
@Override
public @NonNull ObjectSet> long2ObjectEntrySet() {
if(this.entrySet != null) return this.entrySet;
return this.entrySet = new EntrySetView();
}
private void promoteIfNeeded() {
if(this.amended) {
synchronized(this.lock) {
if(this.amended) {
this.promoteLocked();
}
}
}
}
private void promoteLocked() {
if(this.dirty != null) {
this.read = this.dirty;
}
this.amended = false;
this.dirty = null;
this.misses = 0;
}
private void missLocked() {
if(this.misses++ >= (this.dirty != null ? (int) (this.dirty.size() * this.promotionFactor) : 0)) {
this.promoteLocked();
}
}
private void dirtyLocked() {
if(this.dirty != null) return;
this.dirty = this.function.apply(this.read.size());
Long2ObjectMaps.fastForEach(this.read, (entry) -> {
if(!entry.getValue().tryMarkExpunged()) {
this.dirty.put(entry.getLongKey(), entry.getValue());
}
});
}
@SuppressWarnings({"unchecked", "rawtypes"})
private final static class ExpungingValueImpl implements Long2ObjectSyncMap.ExpungingValue {
private static final AtomicReferenceFieldUpdater VALUE_UPDATER = AtomicReferenceFieldUpdater
.newUpdater(ExpungingValueImpl.class, Object.class, "value");
private static final Object EXPUNGED = new Object();
private volatile Object value;
private ExpungingValueImpl(final @NonNull V value) {
this.value = value;
}
@Override
public @Nullable V get() {
final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
return value == ExpungingValueImpl.EXPUNGED ? null : (V) value;
}
@Override
public Map.@NonNull Entry putIfAbsent(final @NonNull V value) {
for(; ; ) {
final Object previous = ExpungingValueImpl.VALUE_UPDATER.get(this);
if(previous == ExpungingValueImpl.EXPUNGED) {
return new AbstractMap.SimpleImmutableEntry<>(Boolean.FALSE, null);
}
if(previous != null) {
return new AbstractMap.SimpleImmutableEntry<>(Boolean.TRUE, (V) previous);
}
if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, null, value)) {
return new AbstractMap.SimpleImmutableEntry<>(Boolean.TRUE, null);
}
}
}
@Override
public boolean exists() {
final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
return value != null && value != ExpungingValueImpl.EXPUNGED;
}
@Override
public @NonNull V set(final @NonNull V value) {
ExpungingValueImpl.VALUE_UPDATER.set(this, value);
return value;
}
@Override
public boolean replace(final @NonNull Object compare, final @Nullable V newValue) {
for(; ; ) {
final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
if(value == ExpungingValueImpl.EXPUNGED || !Objects.equals(value, compare)) return false;
if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, value, newValue)) return true;
}
}
@Override
public @Nullable V clear() {
for(; ; ) {
final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
if(value == null || value == ExpungingValueImpl.EXPUNGED) return null;
if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, value, null)) return (V) value;
}
}
@Override
public boolean trySet(final @NonNull V value) {
for(; ; ) {
final Object present = ExpungingValueImpl.VALUE_UPDATER.get(this);
if(present == ExpungingValueImpl.EXPUNGED) return false;
if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, present, value)) return true;
}
}
@Override
public boolean tryMarkExpunged() {
Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
while(value == null) {
if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, null, ExpungingValueImpl.EXPUNGED)) return true;
value = ExpungingValueImpl.VALUE_UPDATER.get(this);
}
return false;
}
@Override
public boolean tryUnexpungeAndSet(final @Nullable V value) {
return ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, ExpungingValueImpl.EXPUNGED, value);
}
@Override
public boolean tryUnexpunge() {
return ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, ExpungingValueImpl.EXPUNGED, null);
}
@Override
public String toString() {
return "Long2ObjectSyncMapImpl.ExpungingValue{value=" + this.get() + "}";
}
}
private final class MapEntry implements Long2ObjectMap.Entry {
private final long key;
private V value;
private MapEntry(final long key, final @NonNull V value) {
this.key = key;
this.value = value;
}
@Override
public long getLongKey() {
return this.key;
}
@Override
public @NonNull V getValue() {
return this.value;
}
@Override
public @Nullable V setValue(final @NonNull V value) {
requireNonNull(value, "value");
return Long2ObjectSyncMapImpl.this.put(this.key, this.value = value);
}
@Override
public @NonNull String toString() {
return "Long2ObjectSyncMapImpl.MapEntry{key=" + this.getLongKey() + ", value=" + this.getValue() + "}";
}
@Override
public boolean equals(final @Nullable Object other) {
if(this == other) return true;
if(!(other instanceof Long2ObjectMap.Entry)) return false;
final Long2ObjectMap.Entry> that = (Long2ObjectMap.Entry>) other;
return Objects.equals(this.getLongKey(), that.getLongKey())
&& Objects.equals(this.getValue(), that.getValue());
}
@Override
public int hashCode() {
return Objects.hash(this.getLongKey(), this.getValue());
}
}
private final class EntrySetView extends AbstractObjectSet> {
@Override
public int size() {
return Long2ObjectSyncMapImpl.this.size();
}
@Override
public boolean contains(final @Nullable Object entry) {
if(!(entry instanceof Long2ObjectMap.Entry)) return false;
final Long2ObjectMap.Entry> mapEntry = (Long2ObjectMap.Entry>) entry;
final V value = Long2ObjectSyncMapImpl.this.get(mapEntry.getLongKey());
return value != null && Objects.equals(value, mapEntry.getValue());
}
@Override
public boolean remove(final @Nullable Object entry) {
if(!(entry instanceof Long2ObjectMap.Entry)) return false;
final Long2ObjectMap.Entry> mapEntry = (Long2ObjectMap.Entry>) entry;
return Long2ObjectSyncMapImpl.this.remove(mapEntry.getLongKey()) != null;
}
@Override
public void clear() {
Long2ObjectSyncMapImpl.this.clear();
}
@Override
public @NonNull ObjectIterator> iterator() {
Long2ObjectSyncMapImpl.this.promoteIfNeeded();
return new EntryIterator(Long2ObjectSyncMapImpl.this.read.long2ObjectEntrySet().iterator());
}
}
private final class EntryIterator implements ObjectIterator> {
private final Iterator>> backingIterator;
private Long2ObjectMap.Entry next;
private Long2ObjectMap.Entry current;
private EntryIterator(final @NonNull Iterator>> backingIterator) {
this.backingIterator = backingIterator;
this.next = this.nextValue();
}
@Override
public boolean hasNext() {
return this.next != null;
}
@Override
public Long2ObjectMap.@NonNull Entry next() {
if((this.current = this.next) == null) throw new NoSuchElementException();
this.next = this.nextValue();
return this.current;
}
private Long2ObjectMap.@Nullable Entry nextValue() {
Long2ObjectMap.Entry> entry;
V value;
while(this.backingIterator.hasNext()) {
if((value = (entry = this.backingIterator.next()).getValue().get()) != null) {
return new MapEntry(entry.getLongKey(), value);
}
}
return null;
}
@Override
public void remove() {
if(this.current == null) throw new IllegalStateException();
Long2ObjectSyncMapImpl.this.remove(this.current.getLongKey());
this.current = null;
}
@Override
public void forEachRemaining(final @NonNull Consumer super Long2ObjectMap.Entry> action) {
requireNonNull(action, "action");
if(this.next != null) action.accept(this.next);
this.backingIterator.forEachRemaining(entry -> {
final V value = entry.getValue().get();
if(value != null) action.accept(new MapEntry(entry.getLongKey(), value));
});
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy