1 /* 2 * Copyright (C) 2011 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.collect.MapMakerInternalMap.DRAIN_THRESHOLD; 20 import static com.google.common.collect.MapMakerInternalMapTest.SMALL_MAX_SIZE; 21 import static com.google.common.collect.MapMakerInternalMapTest.allEvictingMakers; 22 import static com.google.common.collect.MapMakerInternalMapTest.assertNotified; 23 import static com.google.common.collect.MapMakerInternalMapTest.checkAndDrainRecencyQueue; 24 import static com.google.common.collect.MapMakerInternalMapTest.checkEvictionQueues; 25 import static com.google.common.collect.MapMakerInternalMapTest.checkExpirationTimes; 26 27 import com.google.common.base.Function; 28 import com.google.common.base.Functions; 29 import com.google.common.collect.MapMaker.ComputingMapAdapter; 30 import com.google.common.collect.MapMaker.RemovalCause; 31 import com.google.common.collect.MapMakerInternalMap.ReferenceEntry; 32 import com.google.common.collect.MapMakerInternalMap.Segment; 33 import com.google.common.collect.MapMakerInternalMapTest.DummyEntry; 34 import com.google.common.collect.MapMakerInternalMapTest.DummyValueReference; 35 import com.google.common.collect.MapMakerInternalMapTest.QueuingRemovalListener; 36 import com.google.common.testing.NullPointerTester; 37 38 import junit.framework.TestCase; 39 40 import java.util.Iterator; 41 import java.util.List; 42 import java.util.Random; 43 import java.util.concurrent.CountDownLatch; 44 import java.util.concurrent.ExecutionException; 45 import java.util.concurrent.TimeUnit; 46 import java.util.concurrent.atomic.AtomicInteger; 47 import java.util.concurrent.atomic.AtomicReferenceArray; 48 49 /** 50 * @author Charles Fry 51 */ 52 public class ComputingConcurrentHashMapTest extends TestCase { 53 makeComputingMap( MapMaker maker, Function<? super K, ? extends V> computingFunction)54 private static <K, V> ComputingConcurrentHashMap<K, V> makeComputingMap( 55 MapMaker maker, Function<? super K, ? extends V> computingFunction) { 56 return new ComputingConcurrentHashMap<K, V>( 57 maker, computingFunction); 58 } 59 makeAdaptedMap( MapMaker maker, Function<? super K, ? extends V> computingFunction)60 private static <K, V> ComputingMapAdapter<K, V> makeAdaptedMap( 61 MapMaker maker, Function<? super K, ? extends V> computingFunction) { 62 return new ComputingMapAdapter<K, V>( 63 maker, computingFunction); 64 } 65 createMapMaker()66 private MapMaker createMapMaker() { 67 MapMaker maker = new MapMaker(); 68 maker.useCustomMap = true; 69 return maker; 70 } 71 72 // constructor tests 73 testComputingFunction()74 public void testComputingFunction() { 75 Function<Object, Object> computingFunction = Functions.identity(); 76 ComputingConcurrentHashMap<Object, Object> map = 77 makeComputingMap(createMapMaker(), computingFunction); 78 assertSame(computingFunction, map.computingFunction); 79 } 80 81 // computation tests 82 testCompute()83 public void testCompute() throws ExecutionException { 84 CountingFunction computingFunction = new CountingFunction(); 85 ComputingConcurrentHashMap<Object, Object> map = 86 makeComputingMap(createMapMaker(), computingFunction); 87 assertEquals(0, computingFunction.getCount()); 88 89 Object key = new Object(); 90 Object value = map.getOrCompute(key); 91 assertEquals(1, computingFunction.getCount()); 92 assertEquals(value, map.getOrCompute(key)); 93 assertEquals(1, computingFunction.getCount()); 94 } 95 testComputeNull()96 public void testComputeNull() { 97 Function<Object, Object> computingFunction = new ConstantLoader<Object, Object>(null); 98 ComputingMapAdapter<Object, Object> map = makeAdaptedMap(createMapMaker(), computingFunction); 99 try { 100 map.get(new Object()); 101 fail(); 102 } catch (NullPointerException expected) {} 103 } 104 testRecordReadOnCompute()105 public void testRecordReadOnCompute() throws ExecutionException { 106 CountingFunction computingFunction = new CountingFunction(); 107 for (MapMaker maker : allEvictingMakers()) { 108 ComputingConcurrentHashMap<Object, Object> map = 109 makeComputingMap(maker.concurrencyLevel(1), computingFunction); 110 Segment<Object, Object> segment = map.segments[0]; 111 List<ReferenceEntry<Object, Object>> writeOrder = Lists.newLinkedList(); 112 List<ReferenceEntry<Object, Object>> readOrder = Lists.newLinkedList(); 113 for (int i = 0; i < SMALL_MAX_SIZE; i++) { 114 Object key = new Object(); 115 int hash = map.hash(key); 116 117 map.getOrCompute(key); 118 ReferenceEntry<Object, Object> entry = segment.getEntry(key, hash); 119 writeOrder.add(entry); 120 readOrder.add(entry); 121 } 122 123 checkEvictionQueues(map, segment, readOrder, writeOrder); 124 checkExpirationTimes(map); 125 assertTrue(segment.recencyQueue.isEmpty()); 126 127 // access some of the elements 128 Random random = new Random(); 129 List<ReferenceEntry<Object, Object>> reads = Lists.newArrayList(); 130 Iterator<ReferenceEntry<Object, Object>> i = readOrder.iterator(); 131 while (i.hasNext()) { 132 ReferenceEntry<Object, Object> entry = i.next(); 133 if (random.nextBoolean()) { 134 map.getOrCompute(entry.getKey()); 135 reads.add(entry); 136 i.remove(); 137 assertTrue(segment.recencyQueue.size() <= DRAIN_THRESHOLD); 138 } 139 } 140 int undrainedIndex = reads.size() - segment.recencyQueue.size(); 141 checkAndDrainRecencyQueue(map, segment, reads.subList(undrainedIndex, reads.size())); 142 readOrder.addAll(reads); 143 144 checkEvictionQueues(map, segment, readOrder, writeOrder); 145 checkExpirationTimes(map); 146 } 147 } 148 testComputeExistingEntry()149 public void testComputeExistingEntry() throws ExecutionException { 150 CountingFunction computingFunction = new CountingFunction(); 151 ComputingConcurrentHashMap<Object, Object> map = 152 makeComputingMap(createMapMaker(), computingFunction); 153 assertEquals(0, computingFunction.getCount()); 154 155 Object key = new Object(); 156 Object value = new Object(); 157 map.put(key, value); 158 159 assertEquals(value, map.getOrCompute(key)); 160 assertEquals(0, computingFunction.getCount()); 161 } 162 testComputePartiallyCollectedKey()163 public void testComputePartiallyCollectedKey() throws ExecutionException { 164 MapMaker maker = createMapMaker().concurrencyLevel(1); 165 CountingFunction computingFunction = new CountingFunction(); 166 ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction); 167 Segment<Object, Object> segment = map.segments[0]; 168 AtomicReferenceArray<ReferenceEntry<Object, Object>> table = segment.table; 169 assertEquals(0, computingFunction.getCount()); 170 171 Object key = new Object(); 172 int hash = map.hash(key); 173 Object value = new Object(); 174 int index = hash & (table.length() - 1); 175 176 DummyEntry<Object, Object> entry = DummyEntry.create(key, hash, null); 177 DummyValueReference<Object, Object> valueRef = DummyValueReference.create(value, entry); 178 entry.setValueReference(valueRef); 179 table.set(index, entry); 180 segment.count++; 181 182 assertSame(value, map.getOrCompute(key)); 183 assertEquals(0, computingFunction.getCount()); 184 assertEquals(1, segment.count); 185 186 entry.clearKey(); 187 assertNotSame(value, map.getOrCompute(key)); 188 assertEquals(1, computingFunction.getCount()); 189 assertEquals(2, segment.count); 190 } 191 testComputePartiallyCollectedValue()192 public void testComputePartiallyCollectedValue() throws ExecutionException { 193 MapMaker maker = createMapMaker().concurrencyLevel(1); 194 CountingFunction computingFunction = new CountingFunction(); 195 ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction); 196 Segment<Object, Object> segment = map.segments[0]; 197 AtomicReferenceArray<ReferenceEntry<Object, Object>> table = segment.table; 198 assertEquals(0, computingFunction.getCount()); 199 200 Object key = new Object(); 201 int hash = map.hash(key); 202 Object value = new Object(); 203 int index = hash & (table.length() - 1); 204 205 DummyEntry<Object, Object> entry = DummyEntry.create(key, hash, null); 206 DummyValueReference<Object, Object> valueRef = DummyValueReference.create(value, entry); 207 entry.setValueReference(valueRef); 208 table.set(index, entry); 209 segment.count++; 210 211 assertSame(value, map.getOrCompute(key)); 212 assertEquals(0, computingFunction.getCount()); 213 assertEquals(1, segment.count); 214 215 valueRef.clear(null); 216 assertNotSame(value, map.getOrCompute(key)); 217 assertEquals(1, computingFunction.getCount()); 218 assertEquals(1, segment.count); 219 } 220 221 @SuppressWarnings("deprecation") // test of deprecated method testComputeExpiredEntry()222 public void testComputeExpiredEntry() throws ExecutionException { 223 MapMaker maker = createMapMaker().expireAfterWrite(1, TimeUnit.NANOSECONDS); 224 CountingFunction computingFunction = new CountingFunction(); 225 ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction); 226 assertEquals(0, computingFunction.getCount()); 227 228 Object key = new Object(); 229 Object one = map.getOrCompute(key); 230 assertEquals(1, computingFunction.getCount()); 231 232 Object two = map.getOrCompute(key); 233 assertNotSame(one, two); 234 assertEquals(2, computingFunction.getCount()); 235 } 236 testRemovalListener_replaced()237 public void testRemovalListener_replaced() { 238 // TODO(user): May be a good candidate to play with the MultithreadedTestCase 239 final CountDownLatch startSignal = new CountDownLatch(1); 240 final CountDownLatch computingSignal = new CountDownLatch(1); 241 final CountDownLatch doneSignal = new CountDownLatch(1); 242 final Object computedObject = new Object(); 243 244 Function<Object, Object> computingFunction = new Function<Object, Object>() { 245 @Override 246 public Object apply(Object key) { 247 computingSignal.countDown(); 248 try { 249 startSignal.await(); 250 } catch (InterruptedException e) { 251 throw new RuntimeException(e); 252 } 253 return computedObject; 254 } 255 }; 256 257 QueuingRemovalListener<Object, Object> listener = 258 new QueuingRemovalListener<Object, Object>(); 259 MapMaker maker = (MapMaker) createMapMaker().removalListener(listener); 260 final ComputingConcurrentHashMap<Object, Object> map = 261 makeComputingMap(maker, computingFunction); 262 assertTrue(listener.isEmpty()); 263 264 final Object one = new Object(); 265 final Object two = new Object(); 266 final Object three = new Object(); 267 268 new Thread() { 269 @Override 270 public void run() { 271 try { 272 map.getOrCompute(one); 273 } catch (ExecutionException e) { 274 throw new RuntimeException(e); 275 } 276 doneSignal.countDown(); 277 } 278 }.start(); 279 280 try { 281 computingSignal.await(); 282 } catch (InterruptedException e) { 283 throw new RuntimeException(e); 284 } 285 286 map.put(one, two); 287 startSignal.countDown(); 288 289 try { 290 doneSignal.await(); 291 } catch (InterruptedException e) { 292 throw new RuntimeException(e); 293 } 294 295 assertNotNull(map.putIfAbsent(one, three)); // force notifications 296 assertNotified(listener, one, computedObject, RemovalCause.REPLACED); 297 assertTrue(listener.isEmpty()); 298 } 299 300 // computing functions 301 302 private static class CountingFunction implements Function<Object, Object> { 303 private final AtomicInteger count = new AtomicInteger(); 304 305 @Override apply(Object from)306 public Object apply(Object from) { 307 count.incrementAndGet(); 308 return new Object(); 309 } 310 getCount()311 public int getCount() { 312 return count.get(); 313 } 314 } 315 testNullParameters()316 public void testNullParameters() throws Exception { 317 NullPointerTester tester = new NullPointerTester(); 318 Function<Object, Object> computingFunction = new IdentityLoader<Object>(); 319 tester.testAllPublicInstanceMethods(makeComputingMap(createMapMaker(), computingFunction)); 320 } 321 322 static final class ConstantLoader<K, V> implements Function<K, V> { 323 private final V constant; 324 ConstantLoader(V constant)325 public ConstantLoader(V constant) { 326 this.constant = constant; 327 } 328 329 @Override apply(K key)330 public V apply(K key) { 331 return constant; 332 } 333 } 334 335 static final class IdentityLoader<T> implements Function<T, T> { 336 @Override apply(T key)337 public T apply(T key) { 338 return key; 339 } 340 } 341 342 } 343