Coverage Summary for Class: ConcurrentHashMultiset (com.google.common.collect)
| Class | Method, % | Line, % |
|---|---|---|
| ConcurrentHashMultiset | 0% (0/25) | 0% (0/143) |
| ConcurrentHashMultiset$1 | 0% (0/6) | 0% (0/6) |
| ConcurrentHashMultiset$2 | 0% (0/2) | 0% (0/9) |
| ConcurrentHashMultiset$3 | 0% (0/4) | 0% (0/7) |
| ConcurrentHashMultiset$EntrySet | 0% (0/5) | 0% (0/7) |
| ConcurrentHashMultiset$FieldSettersHolder | 0% (0/2) | 0% (0/2) |
| Total | 0% (0/44) | 0% (0/174) |
1 /* 2 * Copyright (C) 2007 The Guava Authors 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 static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 import static com.google.common.base.Preconditions.checkState; 22 import static com.google.common.collect.CollectPreconditions.checkNonnegative; 23 24 import com.google.common.annotations.Beta; 25 import com.google.common.annotations.GwtIncompatible; 26 import com.google.common.annotations.VisibleForTesting; 27 import com.google.common.collect.Serialization.FieldSetter; 28 import com.google.common.math.IntMath; 29 import com.google.common.primitives.Ints; 30 import com.google.errorprone.annotations.CanIgnoreReturnValue; 31 import com.google.j2objc.annotations.WeakOuter; 32 import java.io.IOException; 33 import java.io.ObjectInputStream; 34 import java.io.ObjectOutputStream; 35 import java.io.Serializable; 36 import java.util.Collection; 37 import java.util.Iterator; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Set; 41 import java.util.concurrent.ConcurrentHashMap; 42 import java.util.concurrent.ConcurrentMap; 43 import java.util.concurrent.atomic.AtomicInteger; 44 import javax.annotation.CheckForNull; 45 import org.checkerframework.checker.nullness.qual.Nullable; 46 47 /** 48 * A multiset that supports concurrent modifications and that provides atomic versions of most 49 * {@code Multiset} operations (exceptions where noted). Null elements are not supported. 50 * 51 * <p>See the Guava User Guide article on <a href= 52 * "https://github.com/google/guava/wiki/NewCollectionTypesExplained#multiset"> {@code 53 * Multiset}</a>. 54 * 55 * @author Cliff L. Biffle 56 * @author mike nonemacher 57 * @since 2.0 58 */ 59 @GwtIncompatible 60 @ElementTypesAreNonnullByDefault 61 public final class ConcurrentHashMultiset<E> extends AbstractMultiset<E> implements Serializable { 62 63 /* 64 * The ConcurrentHashMultiset's atomic operations are implemented primarily in terms of 65 * AtomicInteger's atomic operations, with some help from ConcurrentMap's atomic operations on 66 * creation and removal (including automatic removal of zeroes). If the modification of an 67 * AtomicInteger results in zero, we compareAndSet the value to zero; if that succeeds, we remove 68 * the entry from the Map. If another operation sees a zero in the map, it knows that the entry is 69 * about to be removed, so this operation may remove it (often by replacing it with a new 70 * AtomicInteger). 71 */ 72 73 /** The number of occurrences of each element. */ 74 private final transient ConcurrentMap<E, AtomicInteger> countMap; 75 76 // This constant allows the deserialization code to set a final field. This holder class 77 // makes sure it is not initialized unless an instance is deserialized. 78 private static class FieldSettersHolder { 79 static final FieldSetter<ConcurrentHashMultiset> COUNT_MAP_FIELD_SETTER = 80 Serialization.getFieldSetter(ConcurrentHashMultiset.class, "countMap"); 81 } 82 83 /** 84 * Creates a new, empty {@code ConcurrentHashMultiset} using the default initial capacity, load 85 * factor, and concurrency settings. 86 */ 87 public static <E> ConcurrentHashMultiset<E> create() { 88 // TODO(schmoe): provide a way to use this class with other (possibly arbitrary) 89 // ConcurrentMap implementors. One possibility is to extract most of this class into 90 // an AbstractConcurrentMapMultiset. 91 return new ConcurrentHashMultiset<E>(new ConcurrentHashMap<E, AtomicInteger>()); 92 } 93 94 /** 95 * Creates a new {@code ConcurrentHashMultiset} containing the specified elements, using the 96 * default initial capacity, load factor, and concurrency settings. 97 * 98 * <p>This implementation is highly efficient when {@code elements} is itself a {@link Multiset}. 99 * 100 * @param elements the elements that the multiset should contain 101 */ 102 public static <E> ConcurrentHashMultiset<E> create(Iterable<? extends E> elements) { 103 ConcurrentHashMultiset<E> multiset = ConcurrentHashMultiset.create(); 104 Iterables.addAll(multiset, elements); 105 return multiset; 106 } 107 108 /** 109 * Creates a new, empty {@code ConcurrentHashMultiset} using {@code countMap} as the internal 110 * backing map. 111 * 112 * <p>This instance will assume ownership of {@code countMap}, and other code should not maintain 113 * references to the map or modify it in any way. 114 * 115 * <p>The returned multiset is serializable if the input map is. 116 * 117 * @param countMap backing map for storing the elements in the multiset and their counts. It must 118 * be empty. 119 * @throws IllegalArgumentException if {@code countMap} is not empty 120 * @since 20.0 121 */ 122 @Beta 123 public static <E> ConcurrentHashMultiset<E> create(ConcurrentMap<E, AtomicInteger> countMap) { 124 return new ConcurrentHashMultiset<E>(countMap); 125 } 126 127 @VisibleForTesting 128 ConcurrentHashMultiset(ConcurrentMap<E, AtomicInteger> countMap) { 129 checkArgument(countMap.isEmpty(), "the backing map (%s) must be empty", countMap); 130 this.countMap = countMap; 131 } 132 133 // Query Operations 134 135 /** 136 * Returns the number of occurrences of {@code element} in this multiset. 137 * 138 * @param element the element to look for 139 * @return the nonnegative number of occurrences of the element 140 */ 141 @Override 142 public int count(@CheckForNull Object element) { 143 AtomicInteger existingCounter = Maps.safeGet(countMap, element); 144 return (existingCounter == null) ? 0 : existingCounter.get(); 145 } 146 147 /** 148 * {@inheritDoc} 149 * 150 * <p>If the data in the multiset is modified by any other threads during this method, it is 151 * undefined which (if any) of these modifications will be reflected in the result. 152 */ 153 @Override 154 public int size() { 155 long sum = 0L; 156 for (AtomicInteger value : countMap.values()) { 157 sum += value.get(); 158 } 159 return Ints.saturatedCast(sum); 160 } 161 162 /* 163 * Note: the superclass toArray() methods assume that size() gives a correct 164 * answer, which ours does not. 165 */ 166 167 @Override 168 public Object[] toArray() { 169 return snapshot().toArray(); 170 } 171 172 @Override 173 /* 174 * Our checker says "found: T[]; required: T[]." That sounds bogus. I discuss a possible reason 175 * for this error in https://github.com/jspecify/checker-framework/issues/10. 176 */ 177 @SuppressWarnings("nullness") 178 public <T extends @Nullable Object> T[] toArray(T[] array) { 179 return snapshot().toArray(array); 180 } 181 182 /* 183 * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but 184 * either of these would recurse back to us again! 185 */ 186 private List<E> snapshot() { 187 List<E> list = Lists.newArrayListWithExpectedSize(size()); 188 for (Multiset.Entry<E> entry : entrySet()) { 189 E element = entry.getElement(); 190 for (int i = entry.getCount(); i > 0; i--) { 191 list.add(element); 192 } 193 } 194 return list; 195 } 196 197 // Modification Operations 198 199 /** 200 * Adds a number of occurrences of the specified element to this multiset. 201 * 202 * @param element the element to add 203 * @param occurrences the number of occurrences to add 204 * @return the previous count of the element before the operation; possibly zero 205 * @throws IllegalArgumentException if {@code occurrences} is negative, or if the resulting amount 206 * would exceed {@link Integer#MAX_VALUE} 207 */ 208 @CanIgnoreReturnValue 209 @Override 210 public int add(E element, int occurrences) { 211 checkNotNull(element); 212 if (occurrences == 0) { 213 return count(element); 214 } 215 CollectPreconditions.checkPositive(occurrences, "occurences"); 216 217 while (true) { 218 AtomicInteger existingCounter = Maps.safeGet(countMap, element); 219 if (existingCounter == null) { 220 existingCounter = countMap.putIfAbsent(element, new AtomicInteger(occurrences)); 221 if (existingCounter == null) { 222 return 0; 223 } 224 // existingCounter != null: fall through to operate against the existing AtomicInteger 225 } 226 227 while (true) { 228 int oldValue = existingCounter.get(); 229 if (oldValue != 0) { 230 try { 231 int newValue = IntMath.checkedAdd(oldValue, occurrences); 232 if (existingCounter.compareAndSet(oldValue, newValue)) { 233 // newValue can't == 0, so no need to check & remove 234 return oldValue; 235 } 236 } catch (ArithmeticException overflow) { 237 throw new IllegalArgumentException( 238 "Overflow adding " + occurrences + " occurrences to a count of " + oldValue); 239 } 240 } else { 241 // In the case of a concurrent remove, we might observe a zero value, which means another 242 // thread is about to remove (element, existingCounter) from the map. Rather than wait, 243 // we can just do that work here. 244 AtomicInteger newCounter = new AtomicInteger(occurrences); 245 if ((countMap.putIfAbsent(element, newCounter) == null) 246 || countMap.replace(element, existingCounter, newCounter)) { 247 return 0; 248 } 249 break; 250 } 251 } 252 253 // If we're still here, there was a race, so just try again. 254 } 255 } 256 257 /** 258 * Removes a number of occurrences of the specified element from this multiset. If the multiset 259 * contains fewer than this number of occurrences to begin with, all occurrences will be removed. 260 * 261 * @param element the element whose occurrences should be removed 262 * @param occurrences the number of occurrences of the element to remove 263 * @return the count of the element before the operation; possibly zero 264 * @throws IllegalArgumentException if {@code occurrences} is negative 265 */ 266 /* 267 * TODO(cpovirk): remove and removeExactly currently accept null inputs only 268 * if occurrences == 0. This satisfies both NullPointerTester and 269 * CollectionRemoveTester.testRemove_nullAllowed, but it's not clear that it's 270 * a good policy, especially because, in order for the test to pass, the 271 * parameter must be misleadingly annotated as @Nullable. I suspect that 272 * we'll want to remove @Nullable, add an eager checkNotNull, and loosen up 273 * testRemove_nullAllowed. 274 */ 275 @CanIgnoreReturnValue 276 @Override 277 public int remove(@CheckForNull Object element, int occurrences) { 278 if (occurrences == 0) { 279 return count(element); 280 } 281 CollectPreconditions.checkPositive(occurrences, "occurences"); 282 283 AtomicInteger existingCounter = Maps.safeGet(countMap, element); 284 if (existingCounter == null) { 285 return 0; 286 } 287 while (true) { 288 int oldValue = existingCounter.get(); 289 if (oldValue != 0) { 290 int newValue = Math.max(0, oldValue - occurrences); 291 if (existingCounter.compareAndSet(oldValue, newValue)) { 292 if (newValue == 0) { 293 // Just CASed to 0; remove the entry to clean up the map. If the removal fails, 294 // another thread has already replaced it with a new counter, which is fine. 295 countMap.remove(element, existingCounter); 296 } 297 return oldValue; 298 } 299 } else { 300 return 0; 301 } 302 } 303 } 304 305 /** 306 * Removes exactly the specified number of occurrences of {@code element}, or makes no change if 307 * this is not possible. 308 * 309 * <p>This method, in contrast to {@link #remove(Object, int)}, has no effect when the element 310 * count is smaller than {@code occurrences}. 311 * 312 * @param element the element to remove 313 * @param occurrences the number of occurrences of {@code element} to remove 314 * @return {@code true} if the removal was possible (including if {@code occurrences} is zero) 315 * @throws IllegalArgumentException if {@code occurrences} is negative 316 */ 317 @CanIgnoreReturnValue 318 public boolean removeExactly(@CheckForNull Object element, int occurrences) { 319 if (occurrences == 0) { 320 return true; 321 } 322 CollectPreconditions.checkPositive(occurrences, "occurences"); 323 324 AtomicInteger existingCounter = Maps.safeGet(countMap, element); 325 if (existingCounter == null) { 326 return false; 327 } 328 while (true) { 329 int oldValue = existingCounter.get(); 330 if (oldValue < occurrences) { 331 return false; 332 } 333 int newValue = oldValue - occurrences; 334 if (existingCounter.compareAndSet(oldValue, newValue)) { 335 if (newValue == 0) { 336 // Just CASed to 0; remove the entry to clean up the map. If the removal fails, 337 // another thread has already replaced it with a new counter, which is fine. 338 countMap.remove(element, existingCounter); 339 } 340 return true; 341 } 342 } 343 } 344 345 /** 346 * Adds or removes occurrences of {@code element} such that the {@link #count} of the element 347 * becomes {@code count}. 348 * 349 * @return the count of {@code element} in the multiset before this call 350 * @throws IllegalArgumentException if {@code count} is negative 351 */ 352 @CanIgnoreReturnValue 353 @Override 354 public int setCount(E element, int count) { 355 checkNotNull(element); 356 checkNonnegative(count, "count"); 357 while (true) { 358 AtomicInteger existingCounter = Maps.safeGet(countMap, element); 359 if (existingCounter == null) { 360 if (count == 0) { 361 return 0; 362 } else { 363 existingCounter = countMap.putIfAbsent(element, new AtomicInteger(count)); 364 if (existingCounter == null) { 365 return 0; 366 } 367 // existingCounter != null: fall through 368 } 369 } 370 371 while (true) { 372 int oldValue = existingCounter.get(); 373 if (oldValue == 0) { 374 if (count == 0) { 375 return 0; 376 } else { 377 AtomicInteger newCounter = new AtomicInteger(count); 378 if ((countMap.putIfAbsent(element, newCounter) == null) 379 || countMap.replace(element, existingCounter, newCounter)) { 380 return 0; 381 } 382 } 383 break; 384 } else { 385 if (existingCounter.compareAndSet(oldValue, count)) { 386 if (count == 0) { 387 // Just CASed to 0; remove the entry to clean up the map. If the removal fails, 388 // another thread has already replaced it with a new counter, which is fine. 389 countMap.remove(element, existingCounter); 390 } 391 return oldValue; 392 } 393 } 394 } 395 } 396 } 397 398 /** 399 * Sets the number of occurrences of {@code element} to {@code newCount}, but only if the count is 400 * currently {@code expectedOldCount}. If {@code element} does not appear in the multiset exactly 401 * {@code expectedOldCount} times, no changes will be made. 402 * 403 * @return {@code true} if the change was successful. This usually indicates that the multiset has 404 * been modified, but not always: in the case that {@code expectedOldCount == newCount}, the 405 * method will return {@code true} if the condition was met. 406 * @throws IllegalArgumentException if {@code expectedOldCount} or {@code newCount} is negative 407 */ 408 @CanIgnoreReturnValue 409 @Override 410 public boolean setCount(E element, int expectedOldCount, int newCount) { 411 checkNotNull(element); 412 checkNonnegative(expectedOldCount, "oldCount"); 413 checkNonnegative(newCount, "newCount"); 414 415 AtomicInteger existingCounter = Maps.safeGet(countMap, element); 416 if (existingCounter == null) { 417 if (expectedOldCount != 0) { 418 return false; 419 } else if (newCount == 0) { 420 return true; 421 } else { 422 // if our write lost the race, it must have lost to a nonzero value, so we can stop 423 return countMap.putIfAbsent(element, new AtomicInteger(newCount)) == null; 424 } 425 } 426 int oldValue = existingCounter.get(); 427 if (oldValue == expectedOldCount) { 428 if (oldValue == 0) { 429 if (newCount == 0) { 430 // Just observed a 0; try to remove the entry to clean up the map 431 countMap.remove(element, existingCounter); 432 return true; 433 } else { 434 AtomicInteger newCounter = new AtomicInteger(newCount); 435 return (countMap.putIfAbsent(element, newCounter) == null) 436 || countMap.replace(element, existingCounter, newCounter); 437 } 438 } else { 439 if (existingCounter.compareAndSet(oldValue, newCount)) { 440 if (newCount == 0) { 441 // Just CASed to 0; remove the entry to clean up the map. If the removal fails, 442 // another thread has already replaced it with a new counter, which is fine. 443 countMap.remove(element, existingCounter); 444 } 445 return true; 446 } 447 } 448 } 449 return false; 450 } 451 452 // Views 453 454 @Override 455 Set<E> createElementSet() { 456 final Set<E> delegate = countMap.keySet(); 457 return new ForwardingSet<E>() { 458 @Override 459 protected Set<E> delegate() { 460 return delegate; 461 } 462 463 @Override 464 public boolean contains(@CheckForNull Object object) { 465 return object != null && Collections2.safeContains(delegate, object); 466 } 467 468 @Override 469 public boolean containsAll(Collection<?> collection) { 470 return standardContainsAll(collection); 471 } 472 473 @Override 474 public boolean remove(@CheckForNull Object object) { 475 return object != null && Collections2.safeRemove(delegate, object); 476 } 477 478 @Override 479 public boolean removeAll(Collection<?> c) { 480 return standardRemoveAll(c); 481 } 482 }; 483 } 484 485 @Override 486 Iterator<E> elementIterator() { 487 throw new AssertionError("should never be called"); 488 } 489 490 /** @deprecated Internal method, use {@link #entrySet()}. */ 491 @Deprecated 492 @Override 493 public Set<Multiset.Entry<E>> createEntrySet() { 494 return new EntrySet(); 495 } 496 497 @Override 498 int distinctElements() { 499 return countMap.size(); 500 } 501 502 @Override 503 public boolean isEmpty() { 504 return countMap.isEmpty(); 505 } 506 507 @Override 508 Iterator<Entry<E>> entryIterator() { 509 // AbstractIterator makes this fairly clean, but it doesn't support remove(). To support 510 // remove(), we create an AbstractIterator, and then use ForwardingIterator to delegate to it. 511 final Iterator<Entry<E>> readOnlyIterator = 512 new AbstractIterator<Entry<E>>() { 513 private final Iterator<Map.Entry<E, AtomicInteger>> mapEntries = 514 countMap.entrySet().iterator(); 515 516 @Override 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 /* 583 * Our checker says "found: T[]; required: T[]." That sounds bogus. I discuss a possible reason 584 * for this error in https://github.com/jspecify/checker-framework/issues/10. 585 */ 586 @SuppressWarnings("nullness") 587 public <T extends @Nullable Object> T[] toArray(T[] array) { 588 return snapshot().toArray(array); 589 } 590 591 private List<Multiset.Entry<E>> snapshot() { 592 List<Multiset.Entry<E>> list = Lists.newArrayListWithExpectedSize(size()); 593 // Not Iterables.addAll(list, this), because that'll forward right back here. 594 Iterators.addAll(list, iterator()); 595 return list; 596 } 597 } 598 599 /** @serialData the ConcurrentMap of elements and their counts. */ 600 private void writeObject(ObjectOutputStream stream) throws IOException { 601 stream.defaultWriteObject(); 602 stream.writeObject(countMap); 603 } 604 605 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { 606 stream.defaultReadObject(); 607 @SuppressWarnings("unchecked") // reading data stored by writeObject 608 ConcurrentMap<E, Integer> deserializedCountMap = 609 (ConcurrentMap<E, Integer>) stream.readObject(); 610 FieldSettersHolder.COUNT_MAP_FIELD_SETTER.set(this, deserializedCountMap); 611 } 612 613 private static final long serialVersionUID = 1; 614 }