001/*
002 * Copyright (C) 2007 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect;
018
019import static com.google.common.base.Preconditions.checkArgument;
020import static com.google.common.base.Preconditions.checkNotNull;
021import static com.google.common.base.Preconditions.checkState;
022import static com.google.common.collect.CollectPreconditions.checkNonnegative;
023import static com.google.common.collect.Lists.newArrayListWithExpectedSize;
024import static com.google.common.collect.Maps.safeGet;
025import static java.lang.Math.max;
026import static java.util.Objects.requireNonNull;
027
028import com.google.common.annotations.GwtIncompatible;
029import com.google.common.annotations.J2ktIncompatible;
030import com.google.common.annotations.VisibleForTesting;
031import com.google.common.collect.Serialization.FieldSetter;
032import com.google.common.math.IntMath;
033import com.google.common.primitives.Ints;
034import com.google.errorprone.annotations.CanIgnoreReturnValue;
035import com.google.j2objc.annotations.WeakOuter;
036import java.io.IOException;
037import java.io.ObjectInputStream;
038import java.io.ObjectOutputStream;
039import java.io.Serializable;
040import java.util.Collection;
041import java.util.Iterator;
042import java.util.List;
043import java.util.Map;
044import java.util.Set;
045import java.util.concurrent.ConcurrentHashMap;
046import java.util.concurrent.ConcurrentMap;
047import java.util.concurrent.atomic.AtomicInteger;
048import org.jspecify.annotations.Nullable;
049
050/**
051 * A multiset that supports concurrent modifications and that provides atomic versions of most
052 * {@code Multiset} operations (exceptions where noted). Null elements are not supported.
053 *
054 * <p>See the Guava User Guide article on <a href=
055 * "https://github.com/google/guava/wiki/NewCollectionTypesExplained#multiset">{@code Multiset}</a>.
056 *
057 * @author Cliff L. Biffle
058 * @author mike nonemacher
059 * @since 2.0
060 */
061@J2ktIncompatible
062@GwtIncompatible
063public final class ConcurrentHashMultiset<E> extends AbstractMultiset<E> implements Serializable {
064
065  /*
066   * The ConcurrentHashMultiset's atomic operations are implemented primarily in terms of
067   * AtomicInteger's atomic operations, with some help from ConcurrentMap's atomic operations on
068   * creation and removal (including automatic removal of zeroes). If the modification of an
069   * AtomicInteger results in zero, we compareAndSet the value to zero; if that succeeds, we remove
070   * the entry from the Map. If another operation sees a zero in the map, it knows that the entry is
071   * about to be removed, so this operation may remove it (often by replacing it with a new
072   * AtomicInteger).
073   */
074
075  /** The number of occurrences of each element. */
076  private final transient ConcurrentMap<E, AtomicInteger> countMap;
077
078  // This constant allows the deserialization code to set a final field. This holder class
079  // makes sure it is not initialized unless an instance is deserialized.
080  private static class FieldSettersHolder {
081    static final FieldSetter<? super ConcurrentHashMultiset<?>> COUNT_MAP_FIELD_SETTER =
082        Serialization.getFieldSetter(ConcurrentHashMultiset.class, "countMap");
083  }
084
085  /**
086   * Creates a new, empty {@code ConcurrentHashMultiset} using the default initial capacity, load
087   * factor, and concurrency settings.
088   */
089  public static <E> ConcurrentHashMultiset<E> create() {
090    // TODO(schmoe): provide a way to use this class with other (possibly arbitrary)
091    // ConcurrentMap implementors. One possibility is to extract most of this class into
092    // an AbstractConcurrentMapMultiset.
093    return new ConcurrentHashMultiset<>(new ConcurrentHashMap<E, AtomicInteger>());
094  }
095
096  /**
097   * Creates a new {@code ConcurrentHashMultiset} containing the specified elements, using the
098   * default initial capacity, load factor, and concurrency settings.
099   *
100   * <p>This implementation is highly efficient when {@code elements} is itself a {@link Multiset}.
101   *
102   * @param elements the elements that the multiset should contain
103   */
104  public static <E> ConcurrentHashMultiset<E> create(Iterable<? extends E> elements) {
105    ConcurrentHashMultiset<E> multiset = ConcurrentHashMultiset.create();
106    Iterables.addAll(multiset, elements);
107    return multiset;
108  }
109
110  /**
111   * Creates a new, empty {@code ConcurrentHashMultiset} using {@code countMap} as the internal
112   * backing map.
113   *
114   * <p>This instance will assume ownership of {@code countMap}, and other code should not maintain
115   * references to the map or modify it in any way.
116   *
117   * <p>The returned multiset is serializable if the input map is.
118   *
119   * @param countMap backing map for storing the elements in the multiset and their counts. It must
120   *     be empty.
121   * @throws IllegalArgumentException if {@code countMap} is not empty
122   * @since 20.0
123   */
124  public static <E> ConcurrentHashMultiset<E> create(ConcurrentMap<E, AtomicInteger> countMap) {
125    return new ConcurrentHashMultiset<>(countMap);
126  }
127
128  @VisibleForTesting
129  ConcurrentHashMultiset(ConcurrentMap<E, AtomicInteger> countMap) {
130    checkArgument(countMap.isEmpty(), "the backing map (%s) must be empty", countMap);
131    this.countMap = countMap;
132  }
133
134  // Query Operations
135
136  /**
137   * Returns the number of occurrences of {@code element} in this multiset.
138   *
139   * @param element the element to look for
140   * @return the nonnegative number of occurrences of the element
141   */
142  @Override
143  public int count(@Nullable Object element) {
144    AtomicInteger existingCounter = safeGet(countMap, element);
145    return (existingCounter == null) ? 0 : existingCounter.get();
146  }
147
148  /**
149   * {@inheritDoc}
150   *
151   * <p>If the data in the multiset is modified by any other threads during this method, it is
152   * undefined which (if any) of these modifications will be reflected in the result.
153   */
154  @Override
155  public int size() {
156    long sum = 0L;
157    for (AtomicInteger value : countMap.values()) {
158      sum += value.get();
159    }
160    return Ints.saturatedCast(sum);
161  }
162
163  /*
164   * Note: the superclass toArray() methods assume that size() gives a correct
165   * answer, which ours does not.
166   */
167
168  @Override
169  public Object[] toArray() {
170    return snapshot().toArray();
171  }
172
173  @Override
174  @SuppressWarnings("nullness") // b/192354773 in our checker affects toArray declarations
175  public <T extends @Nullable Object> T[] toArray(T[] array) {
176    return snapshot().toArray(array);
177  }
178
179  /*
180   * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but
181   * either of these would recurse back to us again!
182   */
183  private List<E> snapshot() {
184    List<E> list = newArrayListWithExpectedSize(size());
185    for (Multiset.Entry<E> entry : entrySet()) {
186      E element = entry.getElement();
187      for (int i = entry.getCount(); i > 0; i--) {
188        list.add(element);
189      }
190    }
191    return list;
192  }
193
194  // Modification Operations
195
196  /**
197   * Adds a number of occurrences of the specified element to this multiset.
198   *
199   * @param element the element to add
200   * @param occurrences the number of occurrences to add
201   * @return the previous count of the element before the operation; possibly zero
202   * @throws IllegalArgumentException if {@code occurrences} is negative, or if the resulting amount
203   *     would exceed {@link Integer#MAX_VALUE}
204   */
205  @CanIgnoreReturnValue
206  @Override
207  public int add(E element, int occurrences) {
208    checkNotNull(element);
209    if (occurrences == 0) {
210      return count(element);
211    }
212    CollectPreconditions.checkPositive(occurrences, "occurrences");
213
214    while (true) {
215      AtomicInteger existingCounter = safeGet(countMap, element);
216      if (existingCounter == null) {
217        existingCounter = countMap.putIfAbsent(element, new AtomicInteger(occurrences));
218        if (existingCounter == null) {
219          return 0;
220        }
221        // existingCounter != null: fall through to operate against the existing AtomicInteger
222      }
223
224      while (true) {
225        int oldValue = existingCounter.get();
226        if (oldValue != 0) {
227          try {
228            int newValue = IntMath.checkedAdd(oldValue, occurrences);
229            if (existingCounter.compareAndSet(oldValue, newValue)) {
230              // newValue can't == 0, so no need to check & remove
231              return oldValue;
232            }
233          } catch (ArithmeticException overflow) {
234            throw new IllegalArgumentException(
235                "Overflow adding " + occurrences + " occurrences to a count of " + oldValue);
236          }
237        } else {
238          // In the case of a concurrent remove, we might observe a zero value, which means another
239          // thread is about to remove (element, existingCounter) from the map. Rather than wait,
240          // we can just do that work here.
241          AtomicInteger newCounter = new AtomicInteger(occurrences);
242          if ((countMap.putIfAbsent(element, newCounter) == null)
243              || countMap.replace(element, existingCounter, newCounter)) {
244            return 0;
245          }
246          break;
247        }
248      }
249
250      // If we're still here, there was a race, so just try again.
251    }
252  }
253
254  /**
255   * Removes a number of occurrences of the specified element from this multiset. If the multiset
256   * contains fewer than this number of occurrences to begin with, all occurrences will be removed.
257   *
258   * @param element the element whose occurrences should be removed
259   * @param occurrences the number of occurrences of the element to remove
260   * @return the count of the element before the operation; possibly zero
261   * @throws IllegalArgumentException if {@code occurrences} is negative
262   */
263  /*
264   * TODO(cpovirk): remove and removeExactly currently accept null inputs only
265   * if occurrences == 0. This satisfies both NullPointerTester and
266   * CollectionRemoveTester.testRemove_nullAllowed, but it's not clear that it's
267   * a good policy, especially because, in order for the test to pass, the
268   * parameter must be misleadingly annotated as @Nullable. I suspect that
269   * we'll want to remove @Nullable, add an eager checkNotNull, and loosen up
270   * testRemove_nullAllowed.
271   */
272  @CanIgnoreReturnValue
273  @Override
274  public int remove(@Nullable Object element, int occurrences) {
275    if (occurrences == 0) {
276      return count(element);
277    }
278    CollectPreconditions.checkPositive(occurrences, "occurrences");
279
280    AtomicInteger existingCounter = safeGet(countMap, element);
281    if (existingCounter == null) {
282      return 0;
283    }
284    while (true) {
285      int oldValue = existingCounter.get();
286      if (oldValue != 0) {
287        int newValue = max(0, oldValue - occurrences);
288        if (existingCounter.compareAndSet(oldValue, newValue)) {
289          if (newValue == 0) {
290            // Just CASed to 0; remove the entry to clean up the map. If the removal fails,
291            // another thread has already replaced it with a new counter, which is fine.
292            countMap.remove(element, existingCounter);
293          }
294          return oldValue;
295        }
296      } else {
297        return 0;
298      }
299    }
300  }
301
302  /**
303   * Removes exactly the specified number of occurrences of {@code element}, or makes no change if
304   * this is not possible.
305   *
306   * <p>This method, in contrast to {@link #remove(Object, int)}, has no effect when the element
307   * count is smaller than {@code occurrences}.
308   *
309   * @param element the element to remove
310   * @param occurrences the number of occurrences of {@code element} to remove
311   * @return {@code true} if the removal was possible (including if {@code occurrences} is zero)
312   * @throws IllegalArgumentException if {@code occurrences} is negative
313   */
314  @CanIgnoreReturnValue
315  public boolean removeExactly(@Nullable Object element, int occurrences) {
316    if (occurrences == 0) {
317      return true;
318    }
319    CollectPreconditions.checkPositive(occurrences, "occurrences");
320
321    AtomicInteger existingCounter = safeGet(countMap, element);
322    if (existingCounter == null) {
323      return false;
324    }
325    while (true) {
326      int oldValue = existingCounter.get();
327      if (oldValue < occurrences) {
328        return false;
329      }
330      int newValue = oldValue - occurrences;
331      if (existingCounter.compareAndSet(oldValue, newValue)) {
332        if (newValue == 0) {
333          // Just CASed to 0; remove the entry to clean up the map. If the removal fails,
334          // another thread has already replaced it with a new counter, which is fine.
335          countMap.remove(element, existingCounter);
336        }
337        return true;
338      }
339    }
340  }
341
342  /**
343   * Adds or removes occurrences of {@code element} such that the {@link #count} of the element
344   * becomes {@code count}.
345   *
346   * @return the count of {@code element} in the multiset before this call
347   * @throws IllegalArgumentException if {@code count} is negative
348   */
349  @CanIgnoreReturnValue
350  @Override
351  public int setCount(E element, int count) {
352    checkNotNull(element);
353    checkNonnegative(count, "count");
354    while (true) {
355      AtomicInteger existingCounter = safeGet(countMap, element);
356      if (existingCounter == null) {
357        if (count == 0) {
358          return 0;
359        } else {
360          existingCounter = countMap.putIfAbsent(element, new AtomicInteger(count));
361          if (existingCounter == null) {
362            return 0;
363          }
364          // existingCounter != null: fall through
365        }
366      }
367
368      while (true) {
369        int oldValue = existingCounter.get();
370        if (oldValue == 0) {
371          if (count == 0) {
372            return 0;
373          } else {
374            AtomicInteger newCounter = new AtomicInteger(count);
375            if ((countMap.putIfAbsent(element, newCounter) == null)
376                || countMap.replace(element, existingCounter, newCounter)) {
377              return 0;
378            }
379          }
380          break;
381        } else {
382          if (existingCounter.compareAndSet(oldValue, count)) {
383            if (count == 0) {
384              // Just CASed to 0; remove the entry to clean up the map. If the removal fails,
385              // another thread has already replaced it with a new counter, which is fine.
386              countMap.remove(element, existingCounter);
387            }
388            return oldValue;
389          }
390        }
391      }
392    }
393  }
394
395  /**
396   * Sets the number of occurrences of {@code element} to {@code newCount}, but only if the count is
397   * currently {@code expectedOldCount}. If {@code element} does not appear in the multiset exactly
398   * {@code expectedOldCount} times, no changes will be made.
399   *
400   * @return {@code true} if the change was successful. This usually indicates that the multiset has
401   *     been modified, but not always: in the case that {@code expectedOldCount == newCount}, the
402   *     method will return {@code true} if the condition was met.
403   * @throws IllegalArgumentException if {@code expectedOldCount} or {@code newCount} is negative
404   */
405  @CanIgnoreReturnValue
406  @Override
407  public boolean setCount(E element, int expectedOldCount, int newCount) {
408    checkNotNull(element);
409    checkNonnegative(expectedOldCount, "oldCount");
410    checkNonnegative(newCount, "newCount");
411
412    AtomicInteger existingCounter = safeGet(countMap, element);
413    if (existingCounter == null) {
414      if (expectedOldCount != 0) {
415        return false;
416      } else if (newCount == 0) {
417        return true;
418      } else {
419        // if our write lost the race, it must have lost to a nonzero value, so we can stop
420        return countMap.putIfAbsent(element, new AtomicInteger(newCount)) == null;
421      }
422    }
423    int oldValue = existingCounter.get();
424    if (oldValue == expectedOldCount) {
425      if (oldValue == 0) {
426        if (newCount == 0) {
427          // Just observed a 0; try to remove the entry to clean up the map
428          countMap.remove(element, existingCounter);
429          return true;
430        } else {
431          AtomicInteger newCounter = new AtomicInteger(newCount);
432          return (countMap.putIfAbsent(element, newCounter) == null)
433              || countMap.replace(element, existingCounter, newCounter);
434        }
435      } else {
436        if (existingCounter.compareAndSet(oldValue, newCount)) {
437          if (newCount == 0) {
438            // Just CASed to 0; remove the entry to clean up the map. If the removal fails,
439            // another thread has already replaced it with a new counter, which is fine.
440            countMap.remove(element, existingCounter);
441          }
442          return true;
443        }
444      }
445    }
446    return false;
447  }
448
449  // Views
450
451  @Override
452  Set<E> createElementSet() {
453    Set<E> delegate = countMap.keySet();
454    return new ForwardingSet<E>() {
455      @Override
456      protected Set<E> delegate() {
457        return delegate;
458      }
459
460      @Override
461      public boolean contains(@Nullable Object object) {
462        return object != null && Collections2.safeContains(delegate, object);
463      }
464
465      @Override
466      public boolean containsAll(Collection<?> collection) {
467        return standardContainsAll(collection);
468      }
469
470      @Override
471      public boolean remove(@Nullable Object object) {
472        return object != null && Collections2.safeRemove(delegate, object);
473      }
474
475      @Override
476      public boolean removeAll(Collection<?> c) {
477        return standardRemoveAll(c);
478      }
479    };
480  }
481
482  @Override
483  Iterator<E> elementIterator() {
484    throw new AssertionError("should never be called");
485  }
486
487  /** @deprecated Internal method, use {@link #entrySet()}. */
488  @Deprecated
489  @Override
490  public Set<Multiset.Entry<E>> createEntrySet() {
491    return new EntrySet();
492  }
493
494  @Override
495  int distinctElements() {
496    return countMap.size();
497  }
498
499  @Override
500  public boolean isEmpty() {
501    return countMap.isEmpty();
502  }
503
504  @Override
505  Iterator<Entry<E>> entryIterator() {
506    // AbstractIterator makes this fairly clean, but it doesn't support remove(). To support
507    // remove(), we create an AbstractIterator, and then use ForwardingIterator to delegate to it.
508    Iterator<Entry<E>> readOnlyIterator =
509        new AbstractIterator<Entry<E>>() {
510          private final Iterator<Map.Entry<E, AtomicInteger>> mapEntries =
511              countMap.entrySet().iterator();
512
513          @Override
514          protected @Nullable Entry<E> computeNext() {
515            while (true) {
516              if (!mapEntries.hasNext()) {
517                return endOfData();
518              }
519              Map.Entry<E, AtomicInteger> mapEntry = mapEntries.next();
520              int count = mapEntry.getValue().get();
521              if (count != 0) {
522                return Multisets.immutableEntry(mapEntry.getKey(), count);
523              }
524            }
525          }
526        };
527
528    return new ForwardingIterator<Entry<E>>() {
529      private @Nullable Entry<E> last;
530
531      @Override
532      protected Iterator<Entry<E>> delegate() {
533        return readOnlyIterator;
534      }
535
536      @Override
537      public Entry<E> next() {
538        last = super.next();
539        return last;
540      }
541
542      @Override
543      public void remove() {
544        checkState(last != null, "no calls to next() since the last call to remove()");
545        ConcurrentHashMultiset.this.setCount(last.getElement(), 0);
546        last = null;
547      }
548    };
549  }
550
551  @Override
552  public Iterator<E> iterator() {
553    return Multisets.iteratorImpl(this);
554  }
555
556  @Override
557  public void clear() {
558    countMap.clear();
559  }
560
561  @WeakOuter
562  private class EntrySet extends AbstractMultiset<E>.EntrySet {
563    @Override
564    ConcurrentHashMultiset<E> multiset() {
565      return ConcurrentHashMultiset.this;
566    }
567
568    /*
569     * Note: the superclass toArray() methods assume that size() gives a correct
570     * answer, which ours does not.
571     */
572
573    @Override
574    public Object[] toArray() {
575      return snapshot().toArray();
576    }
577
578    @Override
579    @SuppressWarnings("nullness") // b/192354773 in our checker affects toArray declarations
580    public <T extends @Nullable Object> T[] toArray(T[] array) {
581      return snapshot().toArray(array);
582    }
583
584    private List<Multiset.Entry<E>> snapshot() {
585      List<Multiset.Entry<E>> list = newArrayListWithExpectedSize(size());
586      // Not Iterables.addAll(list, this), because that'll forward right back here.
587      Iterators.addAll(list, iterator());
588      return list;
589    }
590  }
591
592  /** @serialData the ConcurrentMap of elements and their counts. */
593  private void writeObject(ObjectOutputStream stream) throws IOException {
594    stream.defaultWriteObject();
595    stream.writeObject(countMap);
596  }
597
598  @J2ktIncompatible // serialization
599  private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
600    stream.defaultReadObject();
601    @SuppressWarnings("unchecked") // reading data stored by writeObject
602    ConcurrentMap<E, Integer> deserializedCountMap =
603        (ConcurrentMap<E, Integer>) requireNonNull(stream.readObject());
604    FieldSettersHolder.COUNT_MAP_FIELD_SETTER.set(this, deserializedCountMap);
605  }
606
607  private static final long serialVersionUID = 1;
608}