| /* |
| * Copyright (C) 2011 The Guava Authors |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.common.collect; |
| |
| import static com.google.common.collect.MapMakerInternalMap.DRAIN_THRESHOLD; |
| import static com.google.common.collect.MapMakerInternalMapTest.SMALL_MAX_SIZE; |
| import static com.google.common.collect.MapMakerInternalMapTest.allEvictingMakers; |
| import static com.google.common.collect.MapMakerInternalMapTest.assertNotified; |
| import static com.google.common.collect.MapMakerInternalMapTest.checkAndDrainRecencyQueue; |
| import static com.google.common.collect.MapMakerInternalMapTest.checkEvictionQueues; |
| import static com.google.common.collect.MapMakerInternalMapTest.checkExpirationTimes; |
| |
| import com.google.common.base.Function; |
| import com.google.common.collect.MapMaker.ComputingMapAdapter; |
| import com.google.common.collect.MapMaker.RemovalCause; |
| import com.google.common.collect.MapMakerInternalMap.ReferenceEntry; |
| import com.google.common.collect.MapMakerInternalMap.Segment; |
| import com.google.common.collect.MapMakerInternalMapTest.DummyEntry; |
| import com.google.common.collect.MapMakerInternalMapTest.DummyValueReference; |
| import com.google.common.collect.MapMakerInternalMapTest.QueuingRemovalListener; |
| import com.google.common.testing.NullPointerTester; |
| |
| import junit.framework.TestCase; |
| |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReferenceArray; |
| |
| /** |
| * @author Charles Fry |
| */ |
| public class ComputingConcurrentHashMapTest extends TestCase { |
| |
| private static <K, V> ComputingConcurrentHashMap<K, V> makeComputingMap( |
| MapMaker maker, Function<? super K, ? extends V> computingFunction) { |
| return new ComputingConcurrentHashMap<K, V>( |
| maker, computingFunction); |
| } |
| |
| private static <K, V> ComputingMapAdapter<K, V> makeAdaptedMap( |
| MapMaker maker, Function<? super K, ? extends V> computingFunction) { |
| return new ComputingMapAdapter<K, V>( |
| maker, computingFunction); |
| } |
| |
| private MapMaker createMapMaker() { |
| MapMaker maker = new MapMaker(); |
| maker.useCustomMap = true; |
| return maker; |
| } |
| |
| // constructor tests |
| |
| public void testComputingFunction() { |
| Function<Object, Object> computingFunction = new Function<Object, Object>() { |
| @Override |
| public Object apply(Object from) { |
| return from; |
| } |
| }; |
| ComputingConcurrentHashMap<Object, Object> map = |
| makeComputingMap(createMapMaker(), computingFunction); |
| assertSame(computingFunction, map.computingFunction); |
| } |
| |
| // computation tests |
| |
| public void testCompute() throws ExecutionException { |
| CountingFunction computingFunction = new CountingFunction(); |
| ComputingConcurrentHashMap<Object, Object> map = |
| makeComputingMap(createMapMaker(), computingFunction); |
| assertEquals(0, computingFunction.getCount()); |
| |
| Object key = new Object(); |
| Object value = map.getOrCompute(key); |
| assertEquals(1, computingFunction.getCount()); |
| assertEquals(value, map.getOrCompute(key)); |
| assertEquals(1, computingFunction.getCount()); |
| } |
| |
| public void testComputeNull() { |
| Function<Object, Object> computingFunction = new ConstantLoader<Object, Object>(null); |
| ComputingMapAdapter<Object, Object> map = makeAdaptedMap(createMapMaker(), computingFunction); |
| try { |
| map.get(new Object()); |
| fail(); |
| } catch (NullPointerException expected) {} |
| } |
| |
| public void testRecordReadOnCompute() throws ExecutionException { |
| CountingFunction computingFunction = new CountingFunction(); |
| for (MapMaker maker : allEvictingMakers()) { |
| ComputingConcurrentHashMap<Object, Object> map = |
| makeComputingMap(maker.concurrencyLevel(1), computingFunction); |
| Segment<Object, Object> segment = map.segments[0]; |
| List<ReferenceEntry<Object, Object>> writeOrder = Lists.newLinkedList(); |
| List<ReferenceEntry<Object, Object>> readOrder = Lists.newLinkedList(); |
| for (int i = 0; i < SMALL_MAX_SIZE; i++) { |
| Object key = new Object(); |
| int hash = map.hash(key); |
| |
| map.getOrCompute(key); |
| ReferenceEntry<Object, Object> entry = segment.getEntry(key, hash); |
| writeOrder.add(entry); |
| readOrder.add(entry); |
| } |
| |
| checkEvictionQueues(map, segment, readOrder, writeOrder); |
| checkExpirationTimes(map); |
| assertTrue(segment.recencyQueue.isEmpty()); |
| |
| // access some of the elements |
| Random random = new Random(); |
| List<ReferenceEntry<Object, Object>> reads = Lists.newArrayList(); |
| Iterator<ReferenceEntry<Object, Object>> i = readOrder.iterator(); |
| while (i.hasNext()) { |
| ReferenceEntry<Object, Object> entry = i.next(); |
| if (random.nextBoolean()) { |
| map.getOrCompute(entry.getKey()); |
| reads.add(entry); |
| i.remove(); |
| assertTrue(segment.recencyQueue.size() <= DRAIN_THRESHOLD); |
| } |
| } |
| int undrainedIndex = reads.size() - segment.recencyQueue.size(); |
| checkAndDrainRecencyQueue(map, segment, reads.subList(undrainedIndex, reads.size())); |
| readOrder.addAll(reads); |
| |
| checkEvictionQueues(map, segment, readOrder, writeOrder); |
| checkExpirationTimes(map); |
| } |
| } |
| |
| public void testComputeExistingEntry() throws ExecutionException { |
| CountingFunction computingFunction = new CountingFunction(); |
| ComputingConcurrentHashMap<Object, Object> map = |
| makeComputingMap(createMapMaker(), computingFunction); |
| assertEquals(0, computingFunction.getCount()); |
| |
| Object key = new Object(); |
| Object value = new Object(); |
| map.put(key, value); |
| |
| assertEquals(value, map.getOrCompute(key)); |
| assertEquals(0, computingFunction.getCount()); |
| } |
| |
| public void testComputePartiallyCollectedKey() throws ExecutionException { |
| MapMaker maker = createMapMaker().concurrencyLevel(1); |
| CountingFunction computingFunction = new CountingFunction(); |
| ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction); |
| Segment<Object, Object> segment = map.segments[0]; |
| AtomicReferenceArray<ReferenceEntry<Object, Object>> table = segment.table; |
| assertEquals(0, computingFunction.getCount()); |
| |
| Object key = new Object(); |
| int hash = map.hash(key); |
| Object value = new Object(); |
| int index = hash & (table.length() - 1); |
| |
| DummyEntry<Object, Object> entry = DummyEntry.create(key, hash, null); |
| DummyValueReference<Object, Object> valueRef = DummyValueReference.create(value, entry); |
| entry.setValueReference(valueRef); |
| table.set(index, entry); |
| segment.count++; |
| |
| assertSame(value, map.getOrCompute(key)); |
| assertEquals(0, computingFunction.getCount()); |
| assertEquals(1, segment.count); |
| |
| entry.clearKey(); |
| assertNotSame(value, map.getOrCompute(key)); |
| assertEquals(1, computingFunction.getCount()); |
| assertEquals(2, segment.count); |
| } |
| |
| public void testComputePartiallyCollectedValue() throws ExecutionException { |
| MapMaker maker = createMapMaker().concurrencyLevel(1); |
| CountingFunction computingFunction = new CountingFunction(); |
| ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction); |
| Segment<Object, Object> segment = map.segments[0]; |
| AtomicReferenceArray<ReferenceEntry<Object, Object>> table = segment.table; |
| assertEquals(0, computingFunction.getCount()); |
| |
| Object key = new Object(); |
| int hash = map.hash(key); |
| Object value = new Object(); |
| int index = hash & (table.length() - 1); |
| |
| DummyEntry<Object, Object> entry = DummyEntry.create(key, hash, null); |
| DummyValueReference<Object, Object> valueRef = DummyValueReference.create(value, entry); |
| entry.setValueReference(valueRef); |
| table.set(index, entry); |
| segment.count++; |
| |
| assertSame(value, map.getOrCompute(key)); |
| assertEquals(0, computingFunction.getCount()); |
| assertEquals(1, segment.count); |
| |
| valueRef.clear(null); |
| assertNotSame(value, map.getOrCompute(key)); |
| assertEquals(1, computingFunction.getCount()); |
| assertEquals(1, segment.count); |
| } |
| |
| @SuppressWarnings("deprecation") // test of deprecated method |
| public void testComputeExpiredEntry() throws ExecutionException { |
| MapMaker maker = createMapMaker().expireAfterWrite(1, TimeUnit.NANOSECONDS); |
| CountingFunction computingFunction = new CountingFunction(); |
| ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction); |
| assertEquals(0, computingFunction.getCount()); |
| |
| Object key = new Object(); |
| Object one = map.getOrCompute(key); |
| assertEquals(1, computingFunction.getCount()); |
| |
| Object two = map.getOrCompute(key); |
| assertNotSame(one, two); |
| assertEquals(2, computingFunction.getCount()); |
| } |
| |
| public void testRemovalListener_replaced() { |
| // TODO(user): May be a good candidate to play with the MultithreadedTestCase |
| final CountDownLatch startSignal = new CountDownLatch(1); |
| final CountDownLatch computingSignal = new CountDownLatch(1); |
| final CountDownLatch doneSignal = new CountDownLatch(1); |
| final Object computedObject = new Object(); |
| |
| Function<Object, Object> computingFunction = new Function<Object, Object>() { |
| @Override |
| public Object apply(Object key) { |
| computingSignal.countDown(); |
| try { |
| startSignal.await(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| return computedObject; |
| } |
| }; |
| |
| QueuingRemovalListener<Object, Object> listener = |
| new QueuingRemovalListener<Object, Object>(); |
| MapMaker maker = (MapMaker) createMapMaker().removalListener(listener); |
| final ComputingConcurrentHashMap<Object, Object> map = |
| makeComputingMap(maker, computingFunction); |
| assertTrue(listener.isEmpty()); |
| |
| final Object one = new Object(); |
| final Object two = new Object(); |
| final Object three = new Object(); |
| |
| new Thread() { |
| @Override |
| public void run() { |
| try { |
| map.getOrCompute(one); |
| } catch (ExecutionException e) { |
| throw new RuntimeException(e); |
| } |
| doneSignal.countDown(); |
| } |
| }.start(); |
| |
| try { |
| computingSignal.await(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| |
| map.put(one, two); |
| startSignal.countDown(); |
| |
| try { |
| doneSignal.await(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| |
| assertNotNull(map.putIfAbsent(one, three)); // force notifications |
| assertNotified(listener, one, computedObject, RemovalCause.REPLACED); |
| assertTrue(listener.isEmpty()); |
| } |
| |
| // computing functions |
| |
| private static class CountingFunction implements Function<Object, Object> { |
| private final AtomicInteger count = new AtomicInteger(); |
| |
| @Override |
| public Object apply(Object from) { |
| count.incrementAndGet(); |
| return new Object(); |
| } |
| |
| public int getCount() { |
| return count.get(); |
| } |
| } |
| |
| public void testNullParameters() throws Exception { |
| NullPointerTester tester = new NullPointerTester(); |
| Function<Object, Object> computingFunction = new IdentityLoader<Object>(); |
| tester.testAllPublicInstanceMethods(makeComputingMap(createMapMaker(), computingFunction)); |
| } |
| |
| static final class ConstantLoader<K, V> implements Function<K, V> { |
| private final V constant; |
| |
| public ConstantLoader(V constant) { |
| this.constant = constant; |
| } |
| |
| @Override |
| public V apply(K key) { |
| return constant; |
| } |
| } |
| |
| static final class IdentityLoader<T> implements Function<T, T> { |
| @Override |
| public T apply(T key) { |
| return key; |
| } |
| } |
| |
| } |