001 /* 002 * Copyright (C) 2007 Google Inc. 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 017 package com.google.common.collect; 018 019 import static com.google.common.base.Preconditions.checkArgument; 020 import static com.google.common.collect.Multisets.checkNonnegative; 021 022 import com.google.common.annotations.VisibleForTesting; 023 import com.google.common.collect.Serialization.FieldSetter; 024 import com.google.common.primitives.Ints; 025 026 import java.io.IOException; 027 import java.io.ObjectInputStream; 028 import java.io.ObjectOutputStream; 029 import java.io.Serializable; 030 import java.util.AbstractSet; 031 import java.util.Iterator; 032 import java.util.List; 033 import java.util.Map; 034 import java.util.Set; 035 import java.util.concurrent.ConcurrentHashMap; 036 import java.util.concurrent.ConcurrentMap; 037 038 import javax.annotation.Nullable; 039 040 /** 041 * A multiset that supports concurrent modifications and that provides atomic 042 * versions of most {@code Multiset} operations (exceptions where noted). Null 043 * elements are not supported. 044 * 045 * @author Cliff L. Biffle 046 * @since 2 (imported from Google Collections Library) 047 */ 048 public final class ConcurrentHashMultiset<E> extends AbstractMultiset<E> 049 implements Serializable { 050 /* 051 * The ConcurrentHashMultiset's atomic operations are implemented in terms of 052 * ConcurrentMap's atomic operations. Many of them, such as add(E, int), are 053 * read-modify-write sequences, and so are implemented as loops that wrap 054 * ConcurrentMap's compare-and-set operations (like putIfAbsent). 055 */ 056 057 /** The number of occurrences of each element. */ 058 private final transient ConcurrentMap<E, Integer> countMap; 059 060 // This constant allows the deserialization code to set a final field. This 061 // holder class makes sure it is not initialized unless an instance is 062 // deserialized. 063 private static class FieldSettersHolder { 064 @SuppressWarnings("unchecked") 065 // eclipse doesn't like the raw type here, but it's harmless 066 static final FieldSetter<ConcurrentHashMultiset> COUNT_MAP_FIELD_SETTER 067 = Serialization.getFieldSetter( 068 ConcurrentHashMultiset.class, "countMap"); 069 } 070 071 /** 072 * Creates a new, empty {@code ConcurrentHashMultiset} using the default 073 * initial capacity, load factor, and concurrency settings. 074 */ 075 public static <E> ConcurrentHashMultiset<E> create() { 076 return new ConcurrentHashMultiset<E>(new ConcurrentHashMap<E, Integer>()); 077 } 078 079 /** 080 * Creates a new {@code ConcurrentHashMultiset} containing the specified 081 * elements, using the default initial capacity, load factor, and concurrency 082 * settings. 083 * 084 * @param elements the elements that the multiset should contain 085 */ 086 public static <E> ConcurrentHashMultiset<E> create( 087 Iterable<? extends E> elements) { 088 ConcurrentHashMultiset<E> multiset = ConcurrentHashMultiset.create(); 089 Iterables.addAll(multiset, elements); 090 return multiset; 091 } 092 093 /** 094 * Creates an instance using {@code countMap} to store elements and their 095 * counts. 096 * 097 * <p>This instance will assume ownership of {@code countMap}, and other code 098 * should not maintain references to the map or modify it in any way. 099 * 100 * @param countMap backing map for storing the elements in the multiset and 101 * their counts. It must be empty. 102 * @throws IllegalArgumentException if {@code countMap} is not empty 103 */ 104 @VisibleForTesting ConcurrentHashMultiset( 105 ConcurrentMap<E, Integer> countMap) { 106 checkArgument(countMap.isEmpty()); 107 this.countMap = countMap; 108 } 109 110 // Query Operations 111 112 /** 113 * Returns the number of occurrences of {@code element} in this multiset. 114 * 115 * @param element the element to look for 116 * @return the nonnegative number of occurrences of the element 117 */ 118 @Override public int count(@Nullable Object element) { 119 try { 120 return unbox(countMap.get(element)); 121 } catch (NullPointerException e) { 122 return 0; 123 } catch (ClassCastException e) { 124 return 0; 125 } 126 } 127 128 /** 129 * {@inheritDoc} 130 * 131 * <p>If the data in the multiset is modified by any other threads during this 132 * method, it is undefined which (if any) of these modifications will be 133 * reflected in the result. 134 */ 135 @Override public int size() { 136 long sum = 0L; 137 for (Integer value : countMap.values()) { 138 sum += value; 139 } 140 return Ints.saturatedCast(sum); 141 } 142 143 /* 144 * Note: the superclass toArray() methods assume that size() gives a correct 145 * answer, which ours does not. 146 */ 147 148 @Override public Object[] toArray() { 149 return snapshot().toArray(); 150 } 151 152 @Override public <T> T[] toArray(T[] array) { 153 return snapshot().toArray(array); 154 } 155 156 /* 157 * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but 158 * either of these would recurse back to us again! 159 */ 160 private List<E> snapshot() { 161 List<E> list = Lists.newArrayListWithExpectedSize(size()); 162 for (Multiset.Entry<E> entry : entrySet()) { 163 E element = entry.getElement(); 164 for (int i = entry.getCount(); i > 0; i--) { 165 list.add(element); 166 } 167 } 168 return list; 169 } 170 171 // Modification Operations 172 173 /** 174 * Adds a number of occurrences of the specified element to this multiset. 175 * 176 * @param element the element to add 177 * @param occurrences the number of occurrences to add 178 * @return the previous count of the element before the operation; possibly 179 * zero 180 * @throws IllegalArgumentException if {@code occurrences} is negative, or if 181 * the resulting amount would exceed {@link Integer#MAX_VALUE} 182 */ 183 @Override public int add(E element, int occurrences) { 184 if (occurrences == 0) { 185 return count(element); 186 } 187 checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences); 188 189 while (true) { 190 int current = count(element); 191 if (current == 0) { 192 if (countMap.putIfAbsent(element, occurrences) == null) { 193 return 0; 194 } 195 } else { 196 checkArgument(occurrences <= Integer.MAX_VALUE - current, 197 "Overflow adding %s occurrences to a count of %s", 198 occurrences, current); 199 int next = current + occurrences; 200 if (countMap.replace(element, current, next)) { 201 return current; 202 } 203 } 204 // If we're still here, there was a race, so just try again. 205 } 206 } 207 208 /** 209 * Removes a number of occurrences of the specified element from this 210 * multiset. If the multiset contains fewer than this number of occurrences to 211 * begin with, all occurrences will be removed. 212 * 213 * @param element the element whose occurrences should be removed 214 * @param occurrences the number of occurrences of the element to remove 215 * @return the count of the element before the operation; possibly zero 216 * @throws IllegalArgumentException if {@code occurrences} is negative 217 */ 218 @Override public int remove(@Nullable Object element, int occurrences) { 219 if (occurrences == 0) { 220 return count(element); 221 } 222 checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences); 223 224 while (true) { 225 int current = count(element); 226 if (current == 0) { 227 return 0; 228 } 229 if (occurrences >= current) { 230 if (countMap.remove(element, current)) { 231 return current; 232 } 233 } else { 234 // We know it's an "E" because it already exists in the map. 235 @SuppressWarnings("unchecked") 236 E casted = (E) element; 237 238 if (countMap.replace(casted, current, current - occurrences)) { 239 return current; 240 } 241 } 242 // If we're still here, there was a race, so just try again. 243 } 244 } 245 246 /** 247 * Removes <b>all</b> occurrences of the specified element from this multiset. 248 * This method complements {@link Multiset#remove(Object)}, which removes only 249 * one occurrence at a time. 250 * 251 * @param element the element whose occurrences should all be removed 252 * @return the number of occurrences successfully removed, possibly zero 253 */ 254 private int removeAllOccurrences(@Nullable Object element) { 255 try { 256 return unbox(countMap.remove(element)); 257 } catch (NullPointerException e) { 258 return 0; 259 } catch (ClassCastException e) { 260 return 0; 261 } 262 } 263 264 /** 265 * Removes exactly the specified number of occurrences of {@code element}, or 266 * makes no change if this is not possible. 267 * 268 * <p>This method, in contrast to {@link #remove(Object, int)}, has no effect 269 * when the element count is smaller than {@code occurrences}. 270 * 271 * @param element the element to remove 272 * @param occurrences the number of occurrences of {@code element} to remove 273 * @return {@code true} if the removal was possible (including if {@code 274 * occurrences} is zero) 275 */ 276 public boolean removeExactly(@Nullable Object element, int occurrences) { 277 if (occurrences == 0) { 278 return true; 279 } 280 checkArgument(occurrences > 0, "Invalid occurrences: %s", occurrences); 281 282 while (true) { 283 int current = count(element); 284 if (occurrences > current) { 285 return false; 286 } 287 if (occurrences == current) { 288 if (countMap.remove(element, occurrences)) { 289 return true; 290 } 291 } else { 292 @SuppressWarnings("unchecked") // it's in the map, must be an "E" 293 E casted = (E) element; 294 if (countMap.replace(casted, current, current - occurrences)) { 295 return true; 296 } 297 } 298 // If we're still here, there was a race, so just try again. 299 } 300 } 301 302 /** 303 * Adds or removes occurrences of {@code element} such that the {@link #count} 304 * of the element becomes {@code count}. 305 * 306 * @return the count of {@code element} in the multiset before this call 307 * @throws IllegalArgumentException if {@code count} is negative 308 */ 309 @Override public int setCount(E element, int count) { 310 checkNonnegative(count, "count"); 311 return (count == 0) 312 ? removeAllOccurrences(element) 313 : unbox(countMap.put(element, count)); 314 } 315 316 /** 317 * Sets the number of occurrences of {@code element} to {@code newCount}, but 318 * only if the count is currently {@code oldCount}. If {@code element} does 319 * not appear in the multiset exactly {@code oldCount} times, no changes will 320 * be made. 321 * 322 * @return {@code true} if the change was successful. This usually indicates 323 * that the multiset has been modified, but not always: in the case that 324 * {@code oldCount == newCount}, the method will return {@code true} if 325 * the condition was met. 326 * @throws IllegalArgumentException if {@code oldCount} or {@code newCount} is 327 * negative 328 */ 329 @Override public boolean setCount(E element, int oldCount, int newCount) { 330 checkNonnegative(oldCount, "oldCount"); 331 checkNonnegative(newCount, "newCount"); 332 if (newCount == 0) { 333 if (oldCount == 0) { 334 // No change to make, but must return true if the element is not present 335 return !countMap.containsKey(element); 336 } else { 337 return countMap.remove(element, oldCount); 338 } 339 } 340 if (oldCount == 0) { 341 return countMap.putIfAbsent(element, newCount) == null; 342 } 343 return countMap.replace(element, oldCount, newCount); 344 } 345 346 // Views 347 348 @Override Set<E> createElementSet() { 349 final Set<E> delegate = countMap.keySet(); 350 return new ForwardingSet<E>() { 351 @Override protected Set<E> delegate() { 352 return delegate; 353 } 354 @Override public boolean remove(Object object) { 355 try { 356 return delegate.remove(object); 357 } catch (NullPointerException e) { 358 return false; 359 } catch (ClassCastException e) { 360 return false; 361 } 362 } 363 }; 364 } 365 366 private transient EntrySet entrySet; 367 368 @Override public Set<Multiset.Entry<E>> entrySet() { 369 EntrySet result = entrySet; 370 if (result == null) { 371 entrySet = result = new EntrySet(); 372 } 373 return result; 374 } 375 376 private class EntrySet extends AbstractSet<Multiset.Entry<E>> { 377 @Override public int size() { 378 return countMap.size(); 379 } 380 381 @Override public boolean isEmpty() { 382 return countMap.isEmpty(); 383 } 384 385 @Override public boolean contains(Object object) { 386 if (object instanceof Multiset.Entry) { 387 Multiset.Entry<?> entry = (Multiset.Entry<?>) object; 388 Object element = entry.getElement(); 389 int entryCount = entry.getCount(); 390 return entryCount > 0 && count(element) == entryCount; 391 } 392 return false; 393 } 394 395 @Override public Iterator<Multiset.Entry<E>> iterator() { 396 final Iterator<Map.Entry<E, Integer>> backingIterator 397 = countMap.entrySet().iterator(); 398 return new Iterator<Multiset.Entry<E>>() { 399 public boolean hasNext() { 400 return backingIterator.hasNext(); 401 } 402 403 public Multiset.Entry<E> next() { 404 Map.Entry<E, Integer> backingEntry = backingIterator.next(); 405 return Multisets.immutableEntry( 406 backingEntry.getKey(), backingEntry.getValue()); 407 } 408 409 public void remove() { 410 backingIterator.remove(); 411 } 412 }; 413 } 414 415 /* 416 * Note: the superclass toArray() methods assume that size() gives a correct 417 * answer, which ours does not. 418 */ 419 420 @Override public Object[] toArray() { 421 return snapshot().toArray(); 422 } 423 424 @Override public <T> T[] toArray(T[] array) { 425 return snapshot().toArray(array); 426 } 427 428 /* 429 * We'd love to use 'new ArrayList(this)' or 'list.addAll(this)', but 430 * either of these would recurse back to us again! 431 */ 432 private List<Multiset.Entry<E>> snapshot() { 433 List<Multiset.Entry<E>> list = Lists.newArrayListWithExpectedSize(size()); 434 for (Multiset.Entry<E> entry : this) { 435 list.add(entry); 436 } 437 return list; 438 } 439 440 @Override public boolean remove(Object object) { 441 if (object instanceof Multiset.Entry) { 442 Multiset.Entry<?> entry = (Multiset.Entry<?>) object; 443 Object element = entry.getElement(); 444 int entryCount = entry.getCount(); 445 return countMap.remove(element, entryCount); 446 } 447 return false; 448 } 449 450 @Override public void clear() { 451 countMap.clear(); 452 } 453 454 /** 455 * The hash code is the same as countMap's, though the objects aren't equal. 456 */ 457 @Override public int hashCode() { 458 return countMap.hashCode(); 459 } 460 } 461 462 /** 463 * We use a special form of unboxing that treats null as zero. 464 */ 465 private static int unbox(Integer i) { 466 return (i == null) ? 0 : i; 467 } 468 469 /** 470 * @serialData the number of distinct elements, the first element, its count, 471 * the second element, its count, and so on 472 */ 473 private void writeObject(ObjectOutputStream stream) throws IOException { 474 stream.defaultWriteObject(); 475 // creating HashMultiset to handle concurrent changes 476 Serialization.writeMultiset(HashMultiset.create(this), stream); 477 } 478 479 private void readObject(ObjectInputStream stream) 480 throws IOException, ClassNotFoundException { 481 stream.defaultReadObject(); 482 FieldSettersHolder.COUNT_MAP_FIELD_SETTER.set( 483 this, new ConcurrentHashMap<Object, Object>()); 484 Serialization.populateMultiset(this, stream); 485 } 486 487 private static final long serialVersionUID = 0L; 488 }