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