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}