/* * Copyright (C) 2007 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 com.google.common.collect; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.CollectPreconditions.checkNonnegative; import static com.google.common.collect.CollectPreconditions.checkRemove; import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Serialization.FieldSetter; import com.google.common.math.IntMath; import com.google.common.primitives.Ints; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; /** * A multiset that supports concurrent modifications and that provides atomic versions of most * {@code Multiset} operations (exceptions where noted). Null elements are not supported. * *
See the Guava User Guide article on
* {@code Multiset}.
*
* @author Cliff L. Biffle
* @author mike nonemacher
* @since 2.0 (imported from Google Collections Library)
*/
public final class ConcurrentHashMultiset This implementation is highly efficient when {@code elements} is itself a {@link Multiset}.
*
* @param elements the elements that the multiset should contain
*/
public static If this {@link MapMaker} is configured to use entry eviction of any kind, this eviction
* applies to all occurrences of a given element as a single unit. However, most updates to the
* multiset do not count as map updates at all, since we're usually just mutating the value
* stored in the map, so {@link MapMaker#expireAfterAccess} makes sense (evict the entry that
* was queried or updated longest ago), but {@link MapMaker#expireAfterWrite} doesn't, because
* the eviction time is measured from when we saw the first occurrence of the object.
*
* The returned multiset is serializable but any serialization caveats
* given in {@code MapMaker} apply.
*
* Finally, soft/weak values can be used but are not very useful: the values are created
* internally and not exposed externally, so no one else will have a strong reference to the
* values. Weak keys on the other hand can be useful in some scenarios.
*
* @since 15.0 (source compatible (accepting the since removed {@code GenericMapMaker} class)
* since 7.0)
*/
@Beta
public static This instance will assume ownership of {@code countMap}, and other code
* should not maintain references to the map or modify it in any way.
*
* @param countMap backing map for storing the elements in the multiset and
* their counts. It must be empty.
* @throws IllegalArgumentException if {@code countMap} is not empty
*/
@VisibleForTesting ConcurrentHashMultiset(ConcurrentMap If the data in the multiset is modified by any other threads during this method,
* it is undefined which (if any) of these modifications will be reflected in the result.
*/
@Override public int size() {
long sum = 0L;
for (AtomicInteger value : countMap.values()) {
sum += value.get();
}
return Ints.saturatedCast(sum);
}
/*
* Note: the superclass toArray() methods assume that size() gives a correct
* answer, which ours does not.
*/
@Override public Object[] toArray() {
return snapshot().toArray();
}
@Override public This method, in contrast to {@link #remove(Object, int)}, has no effect when the
* element count is smaller than {@code occurrences}.
*
* @param element the element to remove
* @param occurrences the number of occurrences of {@code element} to remove
* @return {@code true} if the removal was possible (including if {@code occurrences} is zero)
*/
public boolean removeExactly(@Nullable Object element, int occurrences) {
if (occurrences == 0) {
return true;
}
checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
AtomicInteger existingCounter = Maps.safeGet(countMap, element);
if (existingCounter == null) {
return false;
}
while (true) {
int oldValue = existingCounter.get();
if (oldValue < occurrences) {
return false;
}
int newValue = oldValue - occurrences;
if (existingCounter.compareAndSet(oldValue, newValue)) {
if (newValue == 0) {
// Just CASed to 0; remove the entry to clean up the map. If the removal fails,
// another thread has already replaced it with a new counter, which is fine.
countMap.remove(element, existingCounter);
}
return true;
}
}
}
/**
* Adds or removes occurrences of {@code element} such that the {@link #count} of the
* element becomes {@code count}.
*
* @return the count of {@code element} in the multiset before this call
* @throws IllegalArgumentException if {@code count} is negative
*/
@Override public int setCount(E element, int count) {
checkNotNull(element);
checkNonnegative(count, "count");
while (true) {
AtomicInteger existingCounter = Maps.safeGet(countMap, element);
if (existingCounter == null) {
if (count == 0) {
return 0;
} else {
existingCounter = countMap.putIfAbsent(element, new AtomicInteger(count));
if (existingCounter == null) {
return 0;
}
// existingCounter != null: fall through
}
}
while (true) {
int oldValue = existingCounter.get();
if (oldValue == 0) {
if (count == 0) {
return 0;
} else {
AtomicInteger newCounter = new AtomicInteger(count);
if ((countMap.putIfAbsent(element, newCounter) == null)
|| countMap.replace(element, existingCounter, newCounter)) {
return 0;
}
}
break;
} else {
if (existingCounter.compareAndSet(oldValue, count)) {
if (count == 0) {
// Just CASed to 0; remove the entry to clean up the map. If the removal fails,
// another thread has already replaced it with a new counter, which is fine.
countMap.remove(element, existingCounter);
}
return oldValue;
}
}
}
}
}
/**
* Sets the number of occurrences of {@code element} to {@code newCount}, but only if
* the count is currently {@code expectedOldCount}. If {@code element} does not appear
* in the multiset exactly {@code expectedOldCount} times, no changes will be made.
*
* @return {@code true} if the change was successful. This usually indicates
* that the multiset has been modified, but not always: in the case that
* {@code expectedOldCount == newCount}, the method will return {@code true} if
* the condition was met.
* @throws IllegalArgumentException if {@code expectedOldCount} or {@code newCount} is negative
*/
@Override public boolean setCount(E element, int expectedOldCount, int newCount) {
checkNotNull(element);
checkNonnegative(expectedOldCount, "oldCount");
checkNonnegative(newCount, "newCount");
AtomicInteger existingCounter = Maps.safeGet(countMap, element);
if (existingCounter == null) {
if (expectedOldCount != 0) {
return false;
} else if (newCount == 0) {
return true;
} else {
// if our write lost the race, it must have lost to a nonzero value, so we can stop
return countMap.putIfAbsent(element, new AtomicInteger(newCount)) == null;
}
}
int oldValue = existingCounter.get();
if (oldValue == expectedOldCount) {
if (oldValue == 0) {
if (newCount == 0) {
// Just observed a 0; try to remove the entry to clean up the map
countMap.remove(element, existingCounter);
return true;
} else {
AtomicInteger newCounter = new AtomicInteger(newCount);
return (countMap.putIfAbsent(element, newCounter) == null)
|| countMap.replace(element, existingCounter, newCounter);
}
} else {
if (existingCounter.compareAndSet(oldValue, newCount)) {
if (newCount == 0) {
// Just CASed to 0; remove the entry to clean up the map. If the removal fails,
// another thread has already replaced it with a new counter, which is fine.
countMap.remove(element, existingCounter);
}
return true;
}
}
}
return false;
}
// Views
@Override Set