diff --git a/platform/platform-tests/testSrc/com/intellij/util/containers/ContainerUtilCollectionsTest.java b/platform/platform-tests/testSrc/com/intellij/util/containers/ContainerUtilCollectionsTest.java index 99a606abc6c7..d2933046e400 100644 --- a/platform/platform-tests/testSrc/com/intellij/util/containers/ContainerUtilCollectionsTest.java +++ b/platform/platform-tests/testSrc/com/intellij/util/containers/ContainerUtilCollectionsTest.java @@ -21,6 +21,7 @@ import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.util.*; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; // tests various ContainerUtil.create*, ContainerUtil.new*, CollectionFactory.create*, ConcurrentCollectionFactory.create* collections for being really weak/soft/concurrent @RunFirst @@ -839,4 +840,18 @@ public class ContainerUtilCollectionsTest extends Assert { UsefulTestCase.assertEmpty(ContainerUtil.collect(map.entries().iterator())); assertTrue(map.isEmpty()); } + + @Test + public void testEvictionListenerWorks() { + AtomicReference evicted = new AtomicReference<>(); + Map map = CollectionFactory.createConcurrentSoftMap(value -> { + assertTrue(evicted.compareAndSet(null, value)); + }); + Object value = new Object(); + map.put(new Object(), value); + + GCUtil.tryGcSoftlyReachableObjects(() -> map.remove("")!=null/*to call processQueue()*/ || map.isEmpty()); + map.remove(""); // to call processQueue() + assertSame(value, evicted.get()); + } } diff --git a/platform/util/base/src/com/intellij/util/containers/CollectionFactory.java b/platform/util/base/src/com/intellij/util/containers/CollectionFactory.java index ce14d8c942a5..3e2264632350 100644 --- a/platform/util/base/src/com/intellij/util/containers/CollectionFactory.java +++ b/platform/util/base/src/com/intellij/util/containers/CollectionFactory.java @@ -361,7 +361,11 @@ public final class CollectionFactory { @Contract(value = " -> new", pure = true) public static @NotNull ConcurrentMap<@NotNull K, @NotNull V> createConcurrentSoftMap() { - return new ConcurrentSoftHashMap<>(); + return new ConcurrentSoftHashMap<>(null); + } + @Contract(value = "_ -> new", pure = true) + public static @NotNull ConcurrentMap<@NotNull K, @NotNull V> createConcurrentSoftMap(@NotNull Consumer evictionListener) { + return new ConcurrentSoftHashMap<>(evictionListener); } @Contract(value = "_,_,_,_-> new", pure = true) diff --git a/platform/util/base/src/com/intellij/util/containers/ConcurrentRefHashMap.java b/platform/util/base/src/com/intellij/util/containers/ConcurrentRefHashMap.java index 7213ccdff7e7..d07ec3bfa40d 100644 --- a/platform/util/base/src/com/intellij/util/containers/ConcurrentRefHashMap.java +++ b/platform/util/base/src/com/intellij/util/containers/ConcurrentRefHashMap.java @@ -8,6 +8,7 @@ import java.lang.ref.ReferenceQueue; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; /** * Base class for concurrent (soft/weak) key:K -> strong value:V map @@ -19,9 +20,25 @@ abstract class ConcurrentRefHashMap extends AbstractMap implements C private final ConcurrentMap, V> myMap; // hashing strategy must be canonical, we compute corresponding hash codes using our own myHashingStrategy private final @NotNull HashingStrategy myHashingStrategy; - private static final float LOAD_FACTOR = 0.75f; + static final float LOAD_FACTOR = 0.75f; static final int DEFAULT_CAPACITY = 16; static final int DEFAULT_CONCURRENCY_LEVEL = Math.min(Runtime.getRuntime().availableProcessors(), 4); + private final Consumer myEvictionListener; + + ConcurrentRefHashMap(@Nullable Consumer evictionListener) { + myHashingStrategy = this; + myMap = new ConcurrentHashMap<>(DEFAULT_CAPACITY, LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); + myEvictionListener = evictionListener; + } + + ConcurrentRefHashMap(int initialCapacity, + float loadFactor, + int concurrencyLevel, + @Nullable HashingStrategy hashingStrategy) { + myHashingStrategy = hashingStrategy == THIS ? this : (hashingStrategy == null ? HashingStrategy.canonical() : hashingStrategy); + myMap = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel); + myEvictionListener = null; + } @FunctionalInterface interface KeyReference { @@ -47,19 +64,14 @@ abstract class ConcurrentRefHashMap extends AbstractMap implements C boolean processed = false; //noinspection unchecked while ((wk = (KeyReference)myReferenceQueue.poll()) != null) { - myMap.remove(wk); + V v = myMap.remove(wk); + if (myEvictionListener != null) { + myEvictionListener.accept(v); + } processed = true; } return processed; } - ConcurrentRefHashMap() { - this(DEFAULT_CAPACITY); - } - - private ConcurrentRefHashMap(int initialCapacity) { - this(initialCapacity, LOAD_FACTOR); - } - private static final HashingStrategy THIS = new HashingStrategy() { @Override public int hashCode(Object object) { @@ -72,23 +84,6 @@ abstract class ConcurrentRefHashMap extends AbstractMap implements C } }; - private ConcurrentRefHashMap(int initialCapacity, float loadFactor) { - //noinspection unchecked - this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL, (HashingStrategy)THIS); - } - - ConcurrentRefHashMap(@NotNull HashingStrategy hashingStrategy) { - this(DEFAULT_CAPACITY, LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, hashingStrategy); - } - - ConcurrentRefHashMap(int initialCapacity, - float loadFactor, - int concurrencyLevel, - @Nullable HashingStrategy hashingStrategy) { - myHashingStrategy = hashingStrategy == THIS ? this : (hashingStrategy == null ? HashingStrategy.canonical() : hashingStrategy); - myMap = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel); - } - @Override public int size() { return entrySet().size(); diff --git a/platform/util/base/src/com/intellij/util/containers/ConcurrentSoftHashMap.java b/platform/util/base/src/com/intellij/util/containers/ConcurrentSoftHashMap.java index 66ce081a775e..2fcd2eaf49f6 100644 --- a/platform/util/base/src/com/intellij/util/containers/ConcurrentSoftHashMap.java +++ b/platform/util/base/src/com/intellij/util/containers/ConcurrentSoftHashMap.java @@ -2,9 +2,11 @@ package com.intellij.util.containers; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; +import java.util.function.Consumer; /** * Concurrent soft key:K -> strong value:V map @@ -13,7 +15,8 @@ import java.lang.ref.SoftReference; * Use {@link ContainerUtil#createConcurrentSoftMap()} to create this */ final class ConcurrentSoftHashMap extends ConcurrentRefHashMap { - ConcurrentSoftHashMap() { + ConcurrentSoftHashMap(@Nullable Consumer evictionListener) { + super(evictionListener); } ConcurrentSoftHashMap(int initialCapacity, diff --git a/platform/util/base/src/com/intellij/util/containers/ConcurrentWeakHashMap.java b/platform/util/base/src/com/intellij/util/containers/ConcurrentWeakHashMap.java index 0a5033a5f713..0ae2d0753845 100644 --- a/platform/util/base/src/com/intellij/util/containers/ConcurrentWeakHashMap.java +++ b/platform/util/base/src/com/intellij/util/containers/ConcurrentWeakHashMap.java @@ -24,7 +24,7 @@ final class ConcurrentWeakHashMap extends ConcurrentRefHashMap { } ConcurrentWeakHashMap(@NotNull HashingStrategy hashingStrategy) { - super(hashingStrategy); + super(DEFAULT_CAPACITY, LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, hashingStrategy); } private static final class WeakKey extends WeakReference implements KeyReference {