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