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}