• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.collect;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 import static com.google.common.base.Preconditions.checkArgument;
21 import static com.google.common.collect.Multisets.checkNonnegative;
22 import com.google.common.collect.Serialization.FieldSetter;
23 
24 import java.io.IOException;
25 import java.io.ObjectInputStream;
26 import java.io.ObjectOutputStream;
27 import java.io.Serializable;
28 import java.util.AbstractSet;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.ConcurrentMap;
35 
36 import javax.annotation.Nullable;
37 
38 /**
39  * A multiset that supports concurrent modifications and that provides atomic
40  * versions of most {@code Multiset} operations (exceptions where noted). Null
41  * elements are not supported.
42  *
43  * @author Cliff L. Biffle
44  * @since 2010.01.04 <b>stable</b> (imported from Google Collections Library)
45  */
46 public final class ConcurrentHashMultiset<E> extends AbstractMultiset<E>
47     implements Serializable {
48   /*
49    * The ConcurrentHashMultiset's atomic operations are implemented in terms of
50    * ConcurrentMap's atomic operations. Many of them, such as add(E, int), are
51    * read-modify-write sequences, and so are implemented as loops that wrap
52    * ConcurrentMap's compare-and-set operations (like putIfAbsent).
53    */
54 
55   /** The number of occurrences of each element. */
56   private final transient ConcurrentMap<E, Integer> countMap;
57 
58   // This constant allows the deserialization code to set a final field. This
59   // holder class makes sure it is not initialized unless an instance is
60   // deserialized.
61   private static class FieldSettersHolder {
62     @SuppressWarnings("unchecked")
63     // eclipse doesn't like the raw type here, but it's harmless
64     static final FieldSetter<ConcurrentHashMultiset> COUNT_MAP_FIELD_SETTER
65         = Serialization.getFieldSetter(
66             ConcurrentHashMultiset.class, "countMap");
67   }
68 
69   /**
70    * Creates a new, empty {@code ConcurrentHashMultiset} using the default
71    * initial capacity, load factor, and concurrency settings.
72    */
create()73   public static <E> ConcurrentHashMultiset<E> create() {
74     return new ConcurrentHashMultiset<E>(new ConcurrentHashMap<E, Integer>());
75   }
76 
77   /**
78    * Creates a new {@code ConcurrentHashMultiset} containing the specified
79    * elements, using the default initial capacity, load factor, and concurrency
80    * settings.
81    *
82    * @param elements the elements that the multiset should contain
83    */
create( Iterable<? extends E> elements)84   public static <E> ConcurrentHashMultiset<E> create(
85       Iterable<? extends E> elements) {
86     ConcurrentHashMultiset<E> multiset = ConcurrentHashMultiset.create();
87     Iterables.addAll(multiset, elements);
88     return multiset;
89   }
90 
91   /**
92    * Creates an instance using {@code countMap} to store elements and their
93    * counts.
94    *
95    * <p>This instance will assume ownership of {@code countMap}, and other code
96    * should not maintain references to the map or modify it in any way.
97    *
98    * @param countMap backing map for storing the elements in the multiset and
99    *     their counts. It must be empty.
100    * @throws IllegalArgumentException if {@code countMap} is not empty
101    */
ConcurrentHashMultiset( ConcurrentMap<E, Integer> countMap)102   @VisibleForTesting ConcurrentHashMultiset(
103       ConcurrentMap<E, Integer> countMap) {
104     checkArgument(countMap.isEmpty());
105     this.countMap = countMap;
106   }
107 
108   // Query Operations
109 
110   /**
111    * Returns the number of occurrences of {@code element} in this multiset.
112    *
113    * @param element the element to look for
114    * @return the nonnegative number of occurrences of the element
115    */
count(@ullable Object element)116   @Override public int count(@Nullable Object element) {
117     try {
118       return unbox(countMap.get(element));
119     } catch (NullPointerException e) {
120       return 0;
121     } catch (ClassCastException e) {
122       return 0;
123     }
124   }
125 
126   /**
127    * {@inheritDoc}
128    *
129    * <p>If the data in the multiset is modified by any other threads during this
130    * method, it is undefined which (if any) of these modifications will be
131    * reflected in the result.
132    */
size()133   @Override public int size() {
134     long sum = 0L;
135     for (Integer value : countMap.values()) {
136       sum += value;
137     }
138     return (int) Math.min(sum, Integer.MAX_VALUE);
139   }
140 
141   /*
142    * Note: the superclass toArray() methods assume that size() gives a correct
143    * answer, which ours does not.
144    */
145 
toArray()146   @Override public Object[] toArray() {
147     return snapshot().toArray();
148   }
149 
toArray(T[] array)150   @Override public <T> T[] toArray(T[] array) {
151     return snapshot().toArray(array);
152   }
153 
154   /*
155    * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but
156    * either of these would recurse back to us again!
157    */
snapshot()158   private List<E> snapshot() {
159     List<E> list = Lists.newArrayListWithExpectedSize(size());
160     for (Multiset.Entry<E> entry : entrySet()) {
161       E element = entry.getElement();
162       for (int i = entry.getCount(); i > 0; i--) {
163         list.add(element);
164       }
165     }
166     return list;
167   }
168 
169   // Modification Operations
170 
171   /**
172    * Adds a number of occurrences of the specified element to this multiset.
173    *
174    * @param element the element to add
175    * @param occurrences the number of occurrences to add
176    * @return the previous count of the element before the operation; possibly
177    *     zero
178    * @throws IllegalArgumentException if {@code occurrences} is negative, or if
179    *     the resulting amount would exceed {@link Integer#MAX_VALUE}
180    */
add(E element, int occurrences)181   @Override public int add(E element, int occurrences) {
182     if (occurrences == 0) {
183       return count(element);
184     }
185     checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
186 
187     while (true) {
188       int current = count(element);
189       if (current == 0) {
190         if (countMap.putIfAbsent(element, occurrences) == null) {
191           return 0;
192         }
193       } else {
194         checkArgument(occurrences <= Integer.MAX_VALUE - current,
195             "Overflow adding %s occurrences to a count of %s",
196             occurrences, current);
197         int next = current + occurrences;
198         if (countMap.replace(element, current, next)) {
199           return current;
200         }
201       }
202       // If we're still here, there was a race, so just try again.
203     }
204   }
205 
206   /**
207    * Removes a number of occurrences of the specified element from this
208    * multiset. If the multiset contains fewer than this number of occurrences to
209    * begin with, all occurrences will be removed.
210    *
211    * @param element the element whose occurrences should be removed
212    * @param occurrences the number of occurrences of the element to remove
213    * @return the count of the element before the operation; possibly zero
214    * @throws IllegalArgumentException if {@code occurrences} is negative
215    */
remove(@ullable Object element, int occurrences)216   @Override public int remove(@Nullable Object element, int occurrences) {
217     if (occurrences == 0) {
218       return count(element);
219     }
220     checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
221 
222     while (true) {
223       int current = count(element);
224       if (current == 0) {
225         return 0;
226       }
227       if (occurrences >= current) {
228         if (countMap.remove(element, current)) {
229           return current;
230         }
231       } else {
232         // We know it's an "E" because it already exists in the map.
233         @SuppressWarnings("unchecked")
234         E casted = (E) element;
235 
236         if (countMap.replace(casted, current, current - occurrences)) {
237           return current;
238         }
239       }
240       // If we're still here, there was a race, so just try again.
241     }
242   }
243 
244   /**
245    * Removes <b>all</b> occurrences of the specified element from this multiset.
246    * This method complements {@link Multiset#remove(Object)}, which removes only
247    * one occurrence at a time.
248    *
249    * @param element the element whose occurrences should all be removed
250    * @return the number of occurrences successfully removed, possibly zero
251    */
removeAllOccurrences(@ullable Object element)252   private int removeAllOccurrences(@Nullable Object element) {
253     try {
254       return unbox(countMap.remove(element));
255     } catch (NullPointerException e) {
256       return 0;
257     } catch (ClassCastException e) {
258       return 0;
259     }
260   }
261 
262   /**
263    * Removes exactly the specified number of occurrences of {@code element}, or
264    * makes no change if this is not possible.
265    *
266    * <p>This method, in contrast to {@link #remove(Object, int)}, has no effect
267    * when the element count is smaller than {@code occurrences}.
268    *
269    * @param element the element to remove
270    * @param occurrences the number of occurrences of {@code element} to remove
271    * @return {@code true} if the removal was possible (including if {@code
272    *     occurrences} is zero)
273    */
removeExactly(@ullable Object element, int occurrences)274   public boolean removeExactly(@Nullable Object element, int occurrences) {
275     if (occurrences == 0) {
276       return true;
277     }
278     checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences);
279 
280     while (true) {
281       int current = count(element);
282       if (occurrences > current) {
283         return false;
284       }
285       if (occurrences == current) {
286         if (countMap.remove(element, occurrences)) {
287           return true;
288         }
289       } else {
290         @SuppressWarnings("unchecked") // it's in the map, must be an "E"
291         E casted = (E) element;
292         if (countMap.replace(casted, current, current - occurrences)) {
293           return true;
294         }
295       }
296       // If we're still here, there was a race, so just try again.
297     }
298   }
299 
300   /**
301    * Adds or removes occurrences of {@code element} such that the {@link #count}
302    * of the element becomes {@code count}.
303    *
304    * @return the count of {@code element} in the multiset before this call
305    * @throws IllegalArgumentException if {@code count} is negative
306    */
setCount(E element, int count)307   @Override public int setCount(E element, int count) {
308     checkNonnegative(count, "count");
309     return (count == 0)
310         ? removeAllOccurrences(element)
311         : unbox(countMap.put(element, count));
312   }
313 
314   /**
315    * Sets the number of occurrences of {@code element} to {@code newCount}, but
316    * only if the count is currently {@code oldCount}. If {@code element} does
317    * not appear in the multiset exactly {@code oldCount} times, no changes will
318    * be made.
319    *
320    * @return {@code true} if the change was successful. This usually indicates
321    *     that the multiset has been modified, but not always: in the case that
322    *     {@code oldCount == newCount}, the method will return {@code true} if
323    *     the condition was met.
324    * @throws IllegalArgumentException if {@code oldCount} or {@code newCount} is
325    *     negative
326    */
setCount(E element, int oldCount, int newCount)327   @Override public boolean setCount(E element, int oldCount, int newCount) {
328     checkNonnegative(oldCount, "oldCount");
329     checkNonnegative(newCount, "newCount");
330     if (newCount == 0) {
331       if (oldCount == 0) {
332         // No change to make, but must return true if the element is not present
333         return !countMap.containsKey(element);
334       } else {
335         return countMap.remove(element, oldCount);
336       }
337     }
338     if (oldCount == 0) {
339       return countMap.putIfAbsent(element, newCount) == null;
340     }
341     return countMap.replace(element, oldCount, newCount);
342   }
343 
344   // Views
345 
createElementSet()346   @Override Set<E> createElementSet() {
347     final Set<E> delegate = countMap.keySet();
348     return new ForwardingSet<E>() {
349       @Override protected Set<E> delegate() {
350         return delegate;
351       }
352       @Override public boolean remove(Object object) {
353         try {
354           return delegate.remove(object);
355         } catch (NullPointerException e) {
356           return false;
357         } catch (ClassCastException e) {
358           return false;
359         }
360       }
361     };
362   }
363 
364   private transient EntrySet entrySet;
365 
366   @Override public Set<Multiset.Entry<E>> entrySet() {
367     EntrySet result = entrySet;
368     if (result == null) {
369       entrySet = result = new EntrySet();
370     }
371     return result;
372   }
373 
374   private class EntrySet extends AbstractSet<Multiset.Entry<E>> {
375     @Override public int size() {
376       return countMap.size();
377     }
378 
379     @Override public boolean isEmpty() {
380       return countMap.isEmpty();
381     }
382 
383     @Override public boolean contains(Object object) {
384       if (object instanceof Multiset.Entry) {
385         Multiset.Entry<?> entry = (Multiset.Entry<?>) object;
386         Object element = entry.getElement();
387         int entryCount = entry.getCount();
388         return entryCount > 0 && count(element) == entryCount;
389       }
390       return false;
391     }
392 
393     @Override public Iterator<Multiset.Entry<E>> iterator() {
394       final Iterator<Map.Entry<E, Integer>> backingIterator
395           = countMap.entrySet().iterator();
396       return new Iterator<Multiset.Entry<E>>() {
397         public boolean hasNext() {
398           return backingIterator.hasNext();
399         }
400 
401         public Multiset.Entry<E> next() {
402           Map.Entry<E, Integer> backingEntry = backingIterator.next();
403           return Multisets.immutableEntry(
404               backingEntry.getKey(), backingEntry.getValue());
405         }
406 
407         public void remove() {
408           backingIterator.remove();
409         }
410       };
411     }
412 
413     /*
414      * Note: the superclass toArray() methods assume that size() gives a correct
415      * answer, which ours does not.
416      */
417 
418     @Override public Object[] toArray() {
419       return snapshot().toArray();
420     }
421 
422     @Override public <T> T[] toArray(T[] array) {
423       return snapshot().toArray(array);
424     }
425 
426     /*
427      * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but
428      * either of these would recurse back to us again!
429      */
430     private List<Multiset.Entry<E>> snapshot() {
431       List<Multiset.Entry<E>> list = Lists.newArrayListWithExpectedSize(size());
432       for (Multiset.Entry<E> entry : this) {
433         list.add(entry);
434       }
435       return list;
436     }
437 
438     @Override public boolean remove(Object object) {
439       if (object instanceof Multiset.Entry) {
440         Multiset.Entry<?> entry = (Multiset.Entry<?>) object;
441         Object element = entry.getElement();
442         int entryCount = entry.getCount();
443         return countMap.remove(element, entryCount);
444       }
445       return false;
446     }
447 
448     @Override public void clear() {
449       countMap.clear();
450     }
451 
452     /**
453      * The hash code is the same as countMap's, though the objects aren't equal.
454      */
455     @Override public int hashCode() {
456       return countMap.hashCode();
457     }
458   }
459 
460   /**
461    * We use a special form of unboxing that treats null as zero.
462    */
463   private static int unbox(Integer i) {
464     return (i == null) ? 0 : i;
465   }
466 
467   /**
468    * @serialData the number of distinct elements, the first element, its count,
469    *     the second element, its count, and so on
470    */
471   private void writeObject(ObjectOutputStream stream) throws IOException {
472     stream.defaultWriteObject();
473     // creating HashMultiset to handle concurrent changes
474     Serialization.writeMultiset(HashMultiset.create(this), stream);
475   }
476 
477   private void readObject(ObjectInputStream stream)
478       throws IOException, ClassNotFoundException {
479     stream.defaultReadObject();
480     FieldSettersHolder.COUNT_MAP_FIELD_SETTER.set(
481         this, new ConcurrentHashMap<Object, Object>());
482     Serialization.populateMultiset(this, stream);
483   }
484 
485   private static final long serialVersionUID = 0L;
486 }
487