dev.mccue.guava.concurrent.AtomicLongMap Maven / Gradle / Ivy
Show all versions of guava-concurrent Show documentation
/*
* Copyright (C) 2011 The Guava Authors
*
* 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 dev.mccue.guava.concurrent;
import static dev.mccue.guava.base.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongBinaryOperator;
import java.util.function.LongUnaryOperator;
import dev.mccue.jsr305.CheckForNull;
/**
* A map containing {@code long} values that can be atomically updated. While writes to a
* traditional {@code Map} rely on {@code put(K, V)}, the typical mechanism for writing to this map
* is {@code addAndGet(K, long)}, which adds a {@code long} to the value currently associated with
* {@code K}. If a key has not yet been associated with a value, its implicit value is zero.
*
* Most methods in this class treat absent values and zero values identically, as individually
* documented. Exceptions to this are {@code #containsKey}, {@code #size}, {@code #isEmpty}, {@code
* #asMap}, and {@code #toString}.
*
*
Instances of this class may be used by multiple threads concurrently. All operations are
* atomic unless otherwise noted.
*
*
Instances of this class are serializable if the keys are serializable.
*
*
Note: If your values are always positive and less than 2^31, you may wish to use a
* {@code dev.mccue.guava.collect.Multiset} such as {@code
* dev.mccue.guava.collect.ConcurrentHashMultiset} instead.
*
*
Warning: Unlike {@code Multiset}, entries whose values are zero are not automatically
* removed from the map. Instead they must be removed manually with {@code #removeAllZeros}.
*
* @author Charles Fry
* @since 11.0
*/
@ElementTypesAreNonnullByDefault
public final class AtomicLongMap implements Serializable {
private final ConcurrentHashMap map;
private AtomicLongMap(ConcurrentHashMap map) {
this.map = checkNotNull(map);
}
/** Creates an {@code AtomicLongMap}. */
public static AtomicLongMap create() {
return new AtomicLongMap(new ConcurrentHashMap<>());
}
/** Creates an {@code AtomicLongMap} with the same mappings as the specified {@code Map}. */
public static AtomicLongMap create(Map extends K, ? extends Long> m) {
AtomicLongMap result = create();
result.putAll(m);
return result;
}
/**
* Returns the value associated with {@code key}, or zero if there is no value associated with
* {@code key}.
*/
public long get(K key) {
return map.getOrDefault(key, 0L);
}
/**
* Increments by one the value currently associated with {@code key}, and returns the new value.
*/
@CanIgnoreReturnValue
public long incrementAndGet(K key) {
return addAndGet(key, 1);
}
/**
* Decrements by one the value currently associated with {@code key}, and returns the new value.
*/
@CanIgnoreReturnValue
public long decrementAndGet(K key) {
return addAndGet(key, -1);
}
/**
* Adds {@code delta} to the value currently associated with {@code key}, and returns the new
* value.
*/
@CanIgnoreReturnValue
public long addAndGet(K key, long delta) {
return accumulateAndGet(key, delta, Long::sum);
}
/**
* Increments by one the value currently associated with {@code key}, and returns the old value.
*/
@CanIgnoreReturnValue
public long getAndIncrement(K key) {
return getAndAdd(key, 1);
}
/**
* Decrements by one the value currently associated with {@code key}, and returns the old value.
*/
@CanIgnoreReturnValue
public long getAndDecrement(K key) {
return getAndAdd(key, -1);
}
/**
* Adds {@code delta} to the value currently associated with {@code key}, and returns the old
* value.
*/
@CanIgnoreReturnValue
public long getAndAdd(K key, long delta) {
return getAndAccumulate(key, delta, Long::sum);
}
/**
* Updates the value currently associated with {@code key} with the specified function, and
* returns the new value. If there is not currently a value associated with {@code key}, the
* function is applied to {@code 0L}.
*
* @since 21.0
*/
@CanIgnoreReturnValue
public long updateAndGet(K key, LongUnaryOperator updaterFunction) {
checkNotNull(updaterFunction);
Long result =
map.compute(
key,
(k, value) -> updaterFunction.applyAsLong((value == null) ? 0L : value.longValue()));
return requireNonNull(result);
}
/**
* Updates the value currently associated with {@code key} with the specified function, and
* returns the old value. If there is not currently a value associated with {@code key}, the
* function is applied to {@code 0L}.
*
* @since 21.0
*/
@CanIgnoreReturnValue
public long getAndUpdate(K key, LongUnaryOperator updaterFunction) {
checkNotNull(updaterFunction);
AtomicLong holder = new AtomicLong();
map.compute(
key,
(k, value) -> {
long oldValue = (value == null) ? 0L : value.longValue();
holder.set(oldValue);
return updaterFunction.applyAsLong(oldValue);
});
return holder.get();
}
/**
* Updates the value currently associated with {@code key} by combining it with {@code x} via the
* specified accumulator function, returning the new value. The previous value associated with
* {@code key} (or zero, if there is none) is passed as the first argument to {@code
* accumulatorFunction}, and {@code x} is passed as the second argument.
*
* @since 21.0
*/
@CanIgnoreReturnValue
public long accumulateAndGet(K key, long x, LongBinaryOperator accumulatorFunction) {
checkNotNull(accumulatorFunction);
return updateAndGet(key, oldValue -> accumulatorFunction.applyAsLong(oldValue, x));
}
/**
* Updates the value currently associated with {@code key} by combining it with {@code x} via the
* specified accumulator function, returning the old value. The previous value associated with
* {@code key} (or zero, if there is none) is passed as the first argument to {@code
* accumulatorFunction}, and {@code x} is passed as the second argument.
*
* @since 21.0
*/
@CanIgnoreReturnValue
public long getAndAccumulate(K key, long x, LongBinaryOperator accumulatorFunction) {
checkNotNull(accumulatorFunction);
return getAndUpdate(key, oldValue -> accumulatorFunction.applyAsLong(oldValue, x));
}
/**
* Associates {@code newValue} with {@code key} in this map, and returns the value previously
* associated with {@code key}, or zero if there was no such value.
*/
@CanIgnoreReturnValue
public long put(K key, long newValue) {
return getAndUpdate(key, x -> newValue);
}
/**
* Copies all of the mappings from the specified map to this map. The effect of this call is
* equivalent to that of calling {@code put(k, v)} on this map once for each mapping from key
* {@code k} to value {@code v} in the specified map. The behavior of this operation is undefined
* if the specified map is modified while the operation is in progress.
*/
public void putAll(Map extends K, ? extends Long> m) {
m.forEach(this::put);
}
/**
* Removes and returns the value associated with {@code key}. If {@code key} is not in the map,
* this method has no effect and returns zero.
*/
@CanIgnoreReturnValue
public long remove(K key) {
Long result = map.remove(key);
return (result == null) ? 0L : result.longValue();
}
/**
* If {@code (key, value)} is currently in the map, this method removes it and returns true;
* otherwise, this method returns false.
*/
boolean remove(K key, long value) {
return map.remove(key, value);
}
/**
* Atomically remove {@code key} from the map iff its associated value is 0.
*
* @since 20.0
*/
@CanIgnoreReturnValue
public boolean removeIfZero(K key) {
return remove(key, 0);
}
/**
* Removes all mappings from this map whose values are zero.
*
* This method is not atomic: the map may be visible in intermediate states, where some of the
* zero values have been removed and others have not.
*/
public void removeAllZeros() {
map.values().removeIf(x -> x == 0);
}
/**
* Returns the sum of all values in this map.
*
*
This method is not atomic: the sum may or may not include other concurrent operations.
*/
public long sum() {
return map.values().stream().mapToLong(Long::longValue).sum();
}
@LazyInit @CheckForNull private transient Map asMap;
/** Returns a live, read-only view of the map backing this {@code AtomicLongMap}. */
public Map asMap() {
Map result = asMap;
return (result == null) ? asMap = createAsMap() : result;
}
private Map createAsMap() {
return Collections.unmodifiableMap(map);
}
/** Returns true if this map contains a mapping for the specified key. */
public boolean containsKey(Object key) {
return map.containsKey(key);
}
/**
* Returns the number of key-value mappings in this map. If the map contains more than {@code
* Integer.MAX_VALUE} elements, returns {@code Integer.MAX_VALUE}.
*/
public int size() {
return map.size();
}
/** Returns {@code true} if this map contains no key-value mappings. */
public boolean isEmpty() {
return map.isEmpty();
}
/**
* Removes all of the mappings from this map. The map will be empty after this call returns.
*
* This method is not atomic: the map may not be empty after returning if there were concurrent
* writes.
*/
public void clear() {
map.clear();
}
@Override
public String toString() {
return map.toString();
}
/**
* If {@code key} is not already associated with a value or if {@code key} is associated with
* zero, associate it with {@code newValue}. Returns the previous value associated with {@code
* key}, or zero if there was no mapping for {@code key}.
*/
long putIfAbsent(K key, long newValue) {
AtomicBoolean noValue = new AtomicBoolean(false);
Long result =
map.compute(
key,
(k, oldValue) -> {
if (oldValue == null || oldValue == 0) {
noValue.set(true);
return newValue;
} else {
return oldValue;
}
});
return noValue.get() ? 0L : requireNonNull(result).longValue();
}
/**
* If {@code (key, expectedOldValue)} is currently in the map, this method replaces {@code
* expectedOldValue} with {@code newValue} and returns true; otherwise, this method returns false.
*
*
If {@code expectedOldValue} is zero, this method will succeed if {@code (key, zero)} is
* currently in the map, or if {@code key} is not in the map at all.
*/
boolean replace(K key, long expectedOldValue, long newValue) {
if (expectedOldValue == 0L) {
return putIfAbsent(key, newValue) == 0L;
} else {
return map.replace(key, expectedOldValue, newValue);
}
}
}