From dd7a59736afcdd2b026212557cda3293e0bcbc6a Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 26 Jun 2022 16:11:13 +0100 Subject: [PATCH 01/40] use ConcurrentLinkedHashMap as backing for LRUMap --- pom.xml | 6 + .../ConcurrentLinkedHashMap.java | 1601 +++++++++++++++++ .../concurrentlinkedhashmap/EntryWeigher.java | 40 + .../EvictionListener.java | 48 + .../concurrentlinkedhashmap/LinkedDeque.java | 463 +++++ .../concurrentlinkedhashmap/Weigher.java | 39 + .../concurrentlinkedhashmap/Weighers.java | 282 +++ .../jackson/databind/util/LRUMap.java | 57 +- .../databind/TestJDKSerialization.java | 5 +- 9 files changed, 2497 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/databind/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/concurrentlinkedhashmap/EntryWeigher.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/concurrentlinkedhashmap/EvictionListener.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/concurrentlinkedhashmap/LinkedDeque.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/concurrentlinkedhashmap/Weigher.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/concurrentlinkedhashmap/Weighers.java diff --git a/pom.xml b/pom.xml index 2b2d9610f8..66e4141147 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,12 @@ jackson-core ${jackson.version.core} + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + javax.measure diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/CLHMTestlibTests.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/CLHMTestlibTests.java new file mode 100644 index 0000000000..9c7e81c0da --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/CLHMTestlibTests.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public final class CLHMTestlibTests extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite(); + addCLHMViewTests(suite); + return suite; + } + + private static void addCLHMViewTests(TestSuite suite) { + suite.addTest(MapTestFactory.suite("CLHMView", MapTestFactory.synchronousGenerator( + () -> new ConcurrentLinkedHashMap.Builder() + .maximumWeightedCapacity(10).build())) + ); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/MapTestFactory.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/MapTestFactory.java new file mode 100644 index 0000000000..b1eba50772 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/MapTestFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Ben Manes. All Rights Reserved. + * + * 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.fasterxml.jackson.databind.util.concurrentlinkedhashmap; + +import java.util.Map; +import java.util.function.Supplier; + +import com.google.common.collect.testing.ConcurrentMapTestSuiteBuilder; +import com.google.common.collect.testing.TestMapGenerator; +import com.google.common.collect.testing.TestStringMapGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.MapFeature; + +import junit.framework.Test; + +/** + * A JUnit test suite factory for the map tests from Guava's testlib. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +final class MapTestFactory { + + private MapTestFactory() {} + + /** + * Returns a test suite. + * + * @param name the name of the cache type under test + * @param generator the map generator + * @return a suite of tests + */ + public static Test suite(String name, TestMapGenerator generator) { + return ConcurrentMapTestSuiteBuilder + .using(generator) + .named(name) + .withFeatures( + CollectionSize.ANY, + MapFeature.GENERAL_PURPOSE, + MapFeature.ALLOWS_NULL_ENTRY_QUERIES, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE) + .createTestSuite(); + } + + /** Returns a map generator for synchronous values. */ + public static TestStringMapGenerator synchronousGenerator( + Supplier> supplier) { + return new TestStringMapGenerator() { + @Override protected Map create(Map.Entry[] entries) { + Map map = supplier.get(); + for (Map.Entry entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + }; + } +} From 3882f175a1eac2485d8818b27e5a0af7b9020e4d Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 27 Jun 2022 20:26:11 +0100 Subject: [PATCH 10/40] prevent add being used on the map entrySet --- .../util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java index 36426f587f..ae7b058bf6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java @@ -1362,7 +1362,7 @@ public boolean contains(Object obj) { @Override public boolean add(Entry entry) { - return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); + throw new UnsupportedOperationException("ConcurrentLinkedHashMap does not allow add to be called on entrySet()"); } @Override From b2c9b081c929e8961b11597579717c2d17ccb41c Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 15:29:39 +0100 Subject: [PATCH 11/40] Create ConcurrentLinkedHashMapStressTest.java --- .../ConcurrentLinkedHashMapStressTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java new file mode 100644 index 0000000000..e4395b62bc --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; + +import org.junit.Test; + +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +public class ConcurrentLinkedHashMapStressTest { + + //increase these to increase the stress + private static int iterations = 100000; + private static int threads = 20; + private static int waitSeconds = 60; + + @Test + public void testManyEntries() throws Exception { + final int maxEntries = 30; + final int maxKey = 100; + final Random rnd = new Random(); + final ConcurrentLinkedHashMap clhm = + new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); + final Map map = new ConcurrentHashMap<>(); + final ExecutorService executor = Executors.newFixedThreadPool(threads); + try { + for (int i = 0; i < maxKey; i++) { + final int key = i; + executor.submit(() -> { + UUID uuid = UUID.randomUUID(); + clhm.put(key, uuid); + map.put(key, uuid); + }); + } + for (int i = 0; i < iterations; i++) { + executor.submit(() -> { + int key = rnd.nextInt(maxKey); + UUID uuid = UUID.randomUUID(); + clhm.put(key, uuid); + map.put(key, uuid); + }); + } + } finally { + executor.shutdown(); + } + executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); + assertEquals(maxEntries, clhm.size()); + int matched = 0; + for (int i = 0; i < maxKey; i++) { + UUID uuid = clhm.get(i); + if (uuid != null) { + matched++; + assertEquals(map.get(i), uuid); + } + } + assertEquals(maxEntries, matched); + } +} From 0c01d2b1e2c90453c976a4c5d2bee87c3f76b0a3 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 15:59:34 +0100 Subject: [PATCH 12/40] Update ConcurrentLinkedHashMapStressTest.java --- .../ConcurrentLinkedHashMapStressTest.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java index e4395b62bc..54bd12c271 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -2,12 +2,15 @@ import org.junit.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; @@ -27,28 +30,32 @@ public void testManyEntries() throws Exception { final ConcurrentLinkedHashMap clhm = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); final Map map = new ConcurrentHashMap<>(); + final List> futures = new ArrayList<>(); final ExecutorService executor = Executors.newFixedThreadPool(threads); try { for (int i = 0; i < maxKey; i++) { final int key = i; - executor.submit(() -> { + futures.add(executor.submit(() -> { UUID uuid = UUID.randomUUID(); clhm.put(key, uuid); map.put(key, uuid); - }); + })); } for (int i = 0; i < iterations; i++) { - executor.submit(() -> { + futures.add(executor.submit(() -> { int key = rnd.nextInt(maxKey); UUID uuid = UUID.randomUUID(); clhm.put(key, uuid); map.put(key, uuid); - }); + })); } } finally { executor.shutdown(); } executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); + for(Future future : futures) { + future.get(waitSeconds, TimeUnit.SECONDS); + } assertEquals(maxEntries, clhm.size()); int matched = 0; for (int i = 0; i < maxKey; i++) { From 674a407633400694cbadba98c4871343b1c4de24 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 16:15:36 +0100 Subject: [PATCH 13/40] wip --- pom.xml | 6 ++++++ .../ConcurrentLinkedHashMapStressTest.java | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5cfe2e2d53..d1eb56f9e2 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,12 @@ 31.1-jre test + + org.awaitility + awaitility + 4.2.0 + test + javax.measure diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java index 54bd12c271..5f751006f7 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -13,6 +13,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.*; import static org.junit.Assert.assertEquals; public class ConcurrentLinkedHashMapStressTest { @@ -23,7 +24,13 @@ public class ConcurrentLinkedHashMapStressTest { private static int waitSeconds = 60; @Test - public void testManyEntries() throws Exception { + public void test() throws Exception { + for(int i = 0; i < 10; i++) { + testManyEntries(); + } + } + + private void testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); @@ -56,12 +63,19 @@ public void testManyEntries() throws Exception { for(Future future : futures) { future.get(waitSeconds, TimeUnit.SECONDS); } + await().atMost(waitSeconds, TimeUnit.SECONDS).until( + () -> clhm.size() == maxEntries + ); assertEquals(maxEntries, clhm.size()); int matched = 0; for (int i = 0; i < maxKey; i++) { UUID uuid = clhm.get(i); if (uuid != null) { matched++; + final int key = i; + await().atMost(waitSeconds, TimeUnit.SECONDS).until( + () -> uuid == map.get(key) + ); assertEquals(map.get(i), uuid); } } From 53c3205edd5380abc7b1b8e49174ace078323b7c Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 17:52:48 +0100 Subject: [PATCH 14/40] try to make test reliable --- pom.xml | 6 ---- .../ConcurrentLinkedHashMapStressTest.java | 35 +++++-------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index d1eb56f9e2..5cfe2e2d53 100644 --- a/pom.xml +++ b/pom.xml @@ -101,12 +101,6 @@ 31.1-jre test - - org.awaitility - awaitility - 4.2.0 - test - javax.measure diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java index 5f751006f7..588d7f2e35 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -2,18 +2,14 @@ import org.junit.Test; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import static org.awaitility.Awaitility.*; import static org.junit.Assert.assertEquals; public class ConcurrentLinkedHashMapStressTest { @@ -24,59 +20,44 @@ public class ConcurrentLinkedHashMapStressTest { private static int waitSeconds = 60; @Test - public void test() throws Exception { - for(int i = 0; i < 10; i++) { - testManyEntries(); - } - } - - private void testManyEntries() throws Exception { + public void testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); final ConcurrentLinkedHashMap clhm = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); final Map map = new ConcurrentHashMap<>(); - final List> futures = new ArrayList<>(); final ExecutorService executor = Executors.newFixedThreadPool(threads); try { for (int i = 0; i < maxKey; i++) { final int key = i; - futures.add(executor.submit(() -> { + executor.submit(() -> { UUID uuid = UUID.randomUUID(); clhm.put(key, uuid); map.put(key, uuid); - })); + }); } for (int i = 0; i < iterations; i++) { - futures.add(executor.submit(() -> { + executor.submit(() -> { int key = rnd.nextInt(maxKey); UUID uuid = UUID.randomUUID(); clhm.put(key, uuid); map.put(key, uuid); - })); + }); } } finally { executor.shutdown(); } executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); - for(Future future : futures) { - future.get(waitSeconds, TimeUnit.SECONDS); - } - await().atMost(waitSeconds, TimeUnit.SECONDS).until( - () -> clhm.size() == maxEntries - ); + clhm.executor.shutdown(); + clhm.executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); assertEquals(maxEntries, clhm.size()); int matched = 0; for (int i = 0; i < maxKey; i++) { UUID uuid = clhm.get(i); if (uuid != null) { matched++; - final int key = i; - await().atMost(waitSeconds, TimeUnit.SECONDS).until( - () -> uuid == map.get(key) - ); - assertEquals(map.get(i), uuid); + assertEquals(map.get(i), clhm.get(i)); } } assertEquals(maxEntries, matched); From bac364ab4b6c52d32816e7d8168e415ed017d33b Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 18:12:07 +0100 Subject: [PATCH 15/40] Update ConcurrentLinkedHashMapStressTest.java --- .../ConcurrentLinkedHashMapStressTest.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java index 588d7f2e35..c67f7b6de0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -2,6 +2,8 @@ import org.junit.Test; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.Random; import java.util.UUID; @@ -51,15 +53,27 @@ public void testManyEntries() throws Exception { executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); clhm.executor.shutdown(); clhm.executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); - assertEquals(maxEntries, clhm.size()); - int matched = 0; - for (int i = 0; i < maxKey; i++) { - UUID uuid = clhm.get(i); - if (uuid != null) { - matched++; - assertEquals(map.get(i), clhm.get(i)); + final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos(); + boolean assertsFailing = true; + while(assertsFailing) { + try { + assertEquals(maxEntries, clhm.size()); + int matched = 0; + for (int i = 0; i < maxKey; i++) { + UUID uuid = clhm.get(i); + if (uuid != null) { + matched++; + assertEquals(map.get(i), clhm.get(i)); + } + } + assertEquals(maxEntries, matched); + assertsFailing = false; + } catch (Throwable t) { + if (System.nanoTime() > endTime) { + throw t; + } + Thread.sleep(100); } } - assertEquals(maxEntries, matched); } } From 517f4327f0b1b42b51ec2b99ca6ba35a3b31d258 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 18:18:32 +0100 Subject: [PATCH 16/40] Update ConcurrentLinkedHashMapStressTest.java --- .../ConcurrentLinkedHashMapStressTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java index c67f7b6de0..615b7816ed 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -26,6 +26,8 @@ public void testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); + //final com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap clhm = + // new com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); final ConcurrentLinkedHashMap clhm = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); final Map map = new ConcurrentHashMap<>(); @@ -51,8 +53,8 @@ public void testManyEntries() throws Exception { executor.shutdown(); } executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); - clhm.executor.shutdown(); - clhm.executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); + //clhm.executor.shutdown(); + //clhm.executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos(); boolean assertsFailing = true; while(assertsFailing) { From c57d7e19d6f3fb4dcb8e6c6fe96a2c766ba7155f Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 18:25:12 +0100 Subject: [PATCH 17/40] Update ConcurrentLinkedHashMapStressTest.java --- .../ConcurrentLinkedHashMapStressTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java index 615b7816ed..e4c3d78402 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java @@ -26,8 +26,6 @@ public void testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); - //final com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap clhm = - // new com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); final ConcurrentLinkedHashMap clhm = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); final Map map = new ConcurrentHashMap<>(); @@ -53,8 +51,9 @@ public void testManyEntries() throws Exception { executor.shutdown(); } executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); - //clhm.executor.shutdown(); - //clhm.executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); + + //ConcurrentLinkedHashMap runs evictions in the background using its own ExecutorService + //the following code retries the assertions to allow for the evictions to finish final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos(); boolean assertsFailing = true; while(assertsFailing) { From 6936cd999a2f458538902fa4e56f2041a6f077b9 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 23:50:41 +0100 Subject: [PATCH 18/40] rename package --- src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java | 2 +- .../ConcurrentLinkedHashMap.java | 2 +- .../util/{concurrentlinkedhashmap => clhm}/EntryWeigher.java | 2 +- .../{concurrentlinkedhashmap => clhm}/EvictionListener.java | 2 +- .../util/{concurrentlinkedhashmap => clhm}/LinkedDeque.java | 2 +- .../util/{concurrentlinkedhashmap => clhm}/Weigher.java | 2 +- .../util/{concurrentlinkedhashmap => clhm}/Weighers.java | 4 ++-- .../{concurrentlinkedhashmap => clhm}/CLHMTestlibTests.java | 2 +- .../ConcurrentLinkedHashMapStressTest.java | 2 +- .../ConcurrentLinkedHashMapTest.java | 2 +- .../{concurrentlinkedhashmap => clhm}/MapTestFactory.java | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) rename src/main/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/ConcurrentLinkedHashMap.java (99%) rename src/main/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/EntryWeigher.java (94%) rename src/main/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/EvictionListener.java (96%) rename src/main/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/LinkedDeque.java (99%) rename src/main/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/Weigher.java (94%) rename src/main/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/Weighers.java (98%) rename src/test/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/CLHMTestlibTests.java (89%) rename src/test/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/ConcurrentLinkedHashMapStressTest.java (97%) rename src/test/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/ConcurrentLinkedHashMapTest.java (97%) rename src/test/java/com/fasterxml/jackson/databind/util/{concurrentlinkedhashmap => clhm}/MapTestFactory.java (97%) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java index 2a3d7cdaec..e48c0a3e32 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java @@ -1,6 +1,6 @@ package com.fasterxml.jackson.databind.util; -import com.fasterxml.jackson.databind.util.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap; /** * Helper for simple bounded maps used for reusing lookup values. diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java similarity index 99% rename from src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java rename to src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index ae7b058bf6..2293570884 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -32,7 +32,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; import java.io.InvalidObjectException; import java.io.ObjectInputStream; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/EntryWeigher.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EntryWeigher.java similarity index 94% rename from src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/EntryWeigher.java rename to src/main/java/com/fasterxml/jackson/databind/util/clhm/EntryWeigher.java index d42ea2368d..8a812f7199 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/EntryWeigher.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EntryWeigher.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; /** * A class that can determine the weight of an entry. The total weight threshold diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/EvictionListener.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java similarity index 96% rename from src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/EvictionListener.java rename to src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java index 1b2c42d444..0681ef6732 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/EvictionListener.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; /** * A listener registered for notification when an entry is evicted. An instance diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/LinkedDeque.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/LinkedDeque.java similarity index 99% rename from src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/LinkedDeque.java rename to src/main/java/com/fasterxml/jackson/databind/util/clhm/LinkedDeque.java index 43498b7778..efe89f831e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/LinkedDeque.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/LinkedDeque.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; import java.util.AbstractCollection; import java.util.Collection; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/Weigher.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weigher.java similarity index 94% rename from src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/Weigher.java rename to src/main/java/com/fasterxml/jackson/databind/util/clhm/Weigher.java index 8b8f276d38..b5ea850abd 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/Weigher.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weigher.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; /** * A class that can determine the weight of a value. The total weight threshold diff --git a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java similarity index 98% rename from src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/Weighers.java rename to src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java index 0f8ecec712..60b508663f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; -import static com.fasterxml.jackson.databind.util.concurrentlinkedhashmap.ConcurrentLinkedHashMap.checkNotNull; +import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.checkNotNull; import java.io.Serializable; import java.util.Collection; diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/CLHMTestlibTests.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/CLHMTestlibTests.java similarity index 89% rename from src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/CLHMTestlibTests.java rename to src/test/java/com/fasterxml/jackson/databind/util/clhm/CLHMTestlibTests.java index 9c7e81c0da..b8496a2e3c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/CLHMTestlibTests.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/CLHMTestlibTests.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; import junit.framework.Test; import junit.framework.TestCase; diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java similarity index 97% rename from src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java rename to src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java index e4c3d78402..3b4db6a761 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; import org.junit.Test; diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapTest.java similarity index 97% rename from src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapTest.java rename to src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapTest.java index db7aa934a1..a4af3e772d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. ****************************************************************/ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; import org.junit.Test; diff --git a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/MapTestFactory.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/MapTestFactory.java similarity index 97% rename from src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/MapTestFactory.java rename to src/test/java/com/fasterxml/jackson/databind/util/clhm/MapTestFactory.java index b1eba50772..4303155b78 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/concurrentlinkedhashmap/MapTestFactory.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/MapTestFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.concurrentlinkedhashmap; +package com.fasterxml.jackson.databind.util.clhm; import java.util.Map; import java.util.function.Supplier; From 669a5627f44779138ac4be627602ec0432e03fb7 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 28 Jun 2022 23:53:47 +0100 Subject: [PATCH 19/40] Create package-info.java --- .../databind/util/clhm/package-info.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java new file mode 100644 index 0000000000..c01e173804 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011 Google Inc. All Rights Reserved. + * + * 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. + */ + +/** + * This package contains an implementation of a bounded + * {@link java.util.concurrent.ConcurrentMap} data structure. + *

+ * {@link com.fasterxml.jackson.databind.util.clhm.Weigher} is a simple interface + * for determining how many units of capacity an entry consumes. Depending on + * which concrete Weigher class is used, an entry may consume a different amount + * of space within the cache. The + * {@link com.fasterxml.jackson.databind.util.clhm.Weighers} class provides + * utility methods for obtaining the most common kinds of implementations. + *

+ * {@link com.fasterxml.jackson.databind.util.clhm.EvictionListener} provides the + * ability to be notified when an entry is evicted from the map. An eviction + * occurs when the entry was automatically removed due to the map exceeding a + * capacity threshold. It is not called when an entry was explicitly removed. + *

+ * The {@link com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap} + * class supplies an efficient, scalable, thread-safe, bounded map. As with the + * Java Collections Framework the "Concurrent" prefix is used to + * indicate that the map is not governed by a single exclusion lock. + * + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +package com.fasterxml.jackson.databind.util.clhm; From f145f0a95856f8a6c4bda9a0d761cd6c3c2a1828 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 29 Jun 2022 00:00:00 +0100 Subject: [PATCH 20/40] remove some unused weigher implementations --- .../jackson/databind/util/clhm/Weighers.java | 183 +----------------- .../ConcurrentLinkedHashMapStressTest.java | 2 +- 2 files changed, 4 insertions(+), 181 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java index 60b508663f..5a67ecc209 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java @@ -18,11 +18,6 @@ import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.checkNotNull; import java.io.Serializable; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; /** * A common set of {@link Weigher} and {@link EntryWeigher} implementations. @@ -41,7 +36,7 @@ private Weighers() { * A entry weigher backed by the specified weigher. The weight of the value * determines the weight of the entry. * - * @param weigher the weigher to be "wrapped" in a entry weigher. + * @param weigher the weigher to be "wrapped" in an entry weigher. * @return A entry weigher view of the specified weigher. */ public static EntryWeigher asEntryWeigher( @@ -59,7 +54,7 @@ public static EntryWeigher asEntryWeigher( * @return A weigher where a value takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) - public static EntryWeigher entrySingleton() { + static EntryWeigher entrySingleton() { return (EntryWeigher) SingletonEntryWeigher.INSTANCE; } @@ -71,120 +66,10 @@ public static EntryWeigher entrySingleton() { * @return A weigher where a value takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) - public static Weigher singleton() { + static Weigher singleton() { return (Weigher) SingletonWeigher.INSTANCE; } - /** - * A weigher where the value is a byte array and its weight is the number of - * bytes. A map bounded with this weigher will evict when the number of bytes - * exceeds the capacity rather than the number of key-value pairs in the map. - * This allows for restricting the capacity based on the memory-consumption - * and is primarily for usage by dedicated caching servers that hold the - * serialized data. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each byte takes one unit of capacity. - */ - public static Weigher byteArray() { - return ByteArrayWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Iterable} and its weight is the - * number of elements. This weigher only should be used when the alternative - * {@link #collection()} weigher cannot be, as evaluation takes O(n) time. A - * map bounded with this weigher will evict when the total number of elements - * exceeds the capacity rather than the number of key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> iterable() { - return (Weigher>) (Weigher) IterableWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Collection} and its weight is the - * number of elements. A map bounded with this weigher will evict when the - * total number of elements exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> collection() { - return (Weigher>) (Weigher) CollectionWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link List} and its weight is the number - * of elements. A map bounded with this weigher will evict when the total - * number of elements exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> list() { - return (Weigher>) (Weigher) ListWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Set} and its weight is the number - * of elements. A map bounded with this weigher will evict when the total - * number of elements exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> set() { - return (Weigher>) (Weigher) SetWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Map} and its weight is the number of - * entries. A map bounded with this weigher will evict when the total number of - * entries across all values exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each entry takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> map() { - return (Weigher>) (Weigher) MapWeigher.INSTANCE; - } - static final class EntryWeigherView implements EntryWeigher, Serializable { static final long serialVersionUID = 1; final Weigher weigher; @@ -217,66 +102,4 @@ public int weightOf(Object value) { return 1; } } - - enum ByteArrayWeigher implements Weigher { - INSTANCE; - - @Override - public int weightOf(byte[] value) { - return value.length; - } - } - - enum IterableWeigher implements Weigher> { - INSTANCE; - - @Override - public int weightOf(Iterable values) { - if (values instanceof Collection) { - return ((Collection) values).size(); - } - int size = 0; - for (Iterator i = values.iterator(); i.hasNext();) { - i.next(); - size++; - } - return size; - } - } - - enum CollectionWeigher implements Weigher> { - INSTANCE; - - @Override - public int weightOf(Collection values) { - return values.size(); - } - } - - enum ListWeigher implements Weigher> { - INSTANCE; - - @Override - public int weightOf(List values) { - return values.size(); - } - } - - enum SetWeigher implements Weigher> { - INSTANCE; - - @Override - public int weightOf(Set values) { - return values.size(); - } - } - - enum MapWeigher implements Weigher> { - INSTANCE; - - @Override - public int weightOf(Map values) { - return values.size(); - } - } } diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java index 3b4db6a761..b52b6f7e21 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java @@ -52,7 +52,7 @@ public void testManyEntries() throws Exception { } executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); - //ConcurrentLinkedHashMap runs evictions in the background using its own ExecutorService + //ConcurrentLinkedHashMap has its own ExecutorService for some background work //the following code retries the assertions to allow for the evictions to finish final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos(); boolean assertsFailing = true; From 946549a0a03cc82bbba47b3a8d7e4470a7026861 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 29 Jun 2022 00:10:54 +0100 Subject: [PATCH 21/40] stop using apache cayenne version and use latest Ben Mane's code again (without jsr305 annotations) --- .../util/clhm/ConcurrentLinkedHashMap.java | 1571 +++++++---------- 1 file changed, 667 insertions(+), 904 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index 2293570884..fb33b7c753 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -1,22 +1,3 @@ -/***************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * https://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. - ****************************************************************/ - /* * Copyright 2010 Google Inc. All Rights Reserved. * @@ -24,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -34,187 +15,191 @@ */ package com.fasterxml.jackson.databind.util.clhm; +import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.DrainStatus.IDLE; +import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.DrainStatus.PROCESSING; +import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.DrainStatus.REQUIRED; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; + import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; -import java.lang.ref.WeakReference; import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractQueue; import java.util.AbstractSet; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.concurrent.AbstractExecutorService; -import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** - * A hash table supporting full concurrency of retrievals, adjustable expected concurrency - * for updates, and a maximum capacity to bound the map by. This implementation differs - * from {@link ConcurrentHashMap} in that it maintains a page replacement algorithm that - * is used to evict an entry when the map has exceeded its capacity. Unlike the - * Java Collections Framework, this map does not have a publicly visible - * constructor and instances are created through a {@link Builder}. + * A hash table supporting full concurrency of retrievals, adjustable expected + * concurrency for updates, and a maximum capacity to bound the map by. This + * implementation differs from {@link ConcurrentHashMap} in that it maintains a + * page replacement algorithm that is used to evict an entry when the map has + * exceeded its capacity. Unlike the Java Collections Framework, this + * map does not have a publicly visible constructor and instances are created + * through a {@link Builder}. *

- * An entry is evicted from the map when the weighted capacity exceeds its - * maximum weighted capacity threshold. A {@link Weigher} instance determines how - * many units of capacity that a value consumes. The default weigher assigns each value a - * weight of 1 to bound the map by the total number of key-value pairs. A map - * that holds collections may choose to weigh values by the number of elements in the - * collection and bound the map by the total number of elements that it contains. A change - * to a value that modifies its weight requires that an update operation is performed on - * the map. + * An entry is evicted from the map when the weighted capacity exceeds + * its maximum weighted capacity threshold. A {@link EntryWeigher} + * determines how many units of capacity that an entry consumes. The default + * weigher assigns each value a weight of 1 to bound the map by the + * total number of key-value pairs. A map that holds collections may choose to + * weigh values by the number of elements in the collection and bound the map + * by the total number of elements that it contains. A change to a value that + * modifies its weight requires that an update operation is performed on the + * map. *

- * An {@link EvictionListener} may be supplied for notification when an entry is evicted - * from the map. This listener is invoked on a caller's thread and will not block other - * threads from operating on the map. An implementation should be aware that the caller's - * thread will not expect long execution times or failures as a side effect of the - * listener being notified. Execution safety and a fast turn around time can be achieved - * by performing the operation asynchronously, such as by submitting a task to an + * An {@link EvictionListener} may be supplied for notification when an entry + * is evicted from the map. This listener is invoked on a caller's thread and + * will not block other threads from operating on the map. An implementation + * should be aware that the caller's thread will not expect long execution + * times or failures as a side effect of the listener being notified. Execution + * safety and a fast turn around time can be achieved by performing the + * operation asynchronously, such as by submitting a task to an * {@link java.util.concurrent.ExecutorService}. *

- * The concurrency level determines the number of threads that can concurrently - * modify the table. Using a significantly higher or lower value than needed can waste - * space or lead to thread contention, but an estimate within an order of magnitude of the - * ideal value does not usually have a noticeable impact. Because placement in hash tables - * is essentially random, the actual concurrency will vary. + * The concurrency level determines the number of threads that can + * concurrently modify the table. Using a significantly higher or lower value + * than needed can waste space or lead to thread contention, but an estimate + * within an order of magnitude of the ideal value does not usually have a + * noticeable impact. Because placement in hash tables is essentially random, + * the actual concurrency will vary. *

- * This class and its views and iterators implement all of the optional methods - * of the {@link Map} and {@link Iterator} interfaces. + * This class and its views and iterators implement all of the + * optional methods of the {@link Map} and {@link Iterator} + * interfaces. *

- * Like {@link java.util.Hashtable} but unlike {@link HashMap}, this class does - * not allow null to be used as a key or value. Unlike - * {@link java.util.LinkedHashMap}, this class does not provide predictable - * iteration order. A snapshot of the keys and entries may be obtained in ascending and - * descending order of retention. + * Like {@link java.util.Hashtable} but unlike {@link HashMap}, this class + * does not allow null to be used as a key or value. Unlike + * {@link java.util.LinkedHashMap}, this class does not provide + * predictable iteration order. A snapshot of the keys and entries may be + * obtained in ascending and descending order of retention. * * @author ben.manes@gmail.com (Ben Manes) * @param the type of keys maintained by this map * @param the type of mapped values + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ */ -// based on https://github.com/apache/cayenne/blob/b156addac1c8e4079fa88e977fee609210c5da69/cayenne-server/src/main/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java -// which is based on http://concurrentlinkedhashmap.googlecode.com/svn/trunk r754 -public class ConcurrentLinkedHashMap extends AbstractMap implements - ConcurrentMap, Serializable { +//@ThreadSafe +public final class ConcurrentLinkedHashMap extends AbstractMap + implements ConcurrentMap, Serializable { /* * This class performs a best-effort bounding of a ConcurrentHashMap using a - * page-replacement algorithm to determine which entries to evict when the capacity is - * exceeded. The page replacement algorithm's data structures are kept eventually - * consistent with the map. An update to the map and recording of reads may not be - * immediately reflected on the algorithm's data structures. These structures are - * guarded by a lock and operations are applied in batches to avoid lock contention. - * The penalty of applying the batches is spread across threads so that the amortized - * cost is slightly higher than performing just the ConcurrentHashMap operation. A - * memento of the reads and writes that were performed on the map are recorded in a - * buffer. These buffers are drained at the first opportunity after a write or when a - * buffer exceeds a threshold size. A mostly strict ordering is achieved by observing - * that each buffer is in a weakly sorted order relative to the last drain. This - * allows the buffers to be merged in O(n) time so that the operations are run in the - * expected order. Due to a lack of a strict ordering guarantee, a task can be - * executed out-of-order, such as a removal followed by its addition. The state of the - * entry is encoded within the value's weight. Alive: The entry is in both the - * hash-table and the page replacement policy. This is represented by a positive - * weight. Retired: The entry is not in the hash-table and is pending removal from the - * page replacement policy. This is represented by a negative weight. Dead: The entry - * is not in the hash-table and is not in the page replacement policy. This is - * represented by a weight of zero. The Least Recently Used page replacement algorithm - * was chosen due to its simplicity, high hit rate, and ability to be implemented with - * O(1) time complexity. + * page-replacement algorithm to determine which entries to evict when the + * capacity is exceeded. + * + * The page replacement algorithm's data structures are kept eventually + * consistent with the map. An update to the map and recording of reads may + * not be immediately reflected on the algorithm's data structures. These + * structures are guarded by a lock and operations are applied in batches to + * avoid lock contention. The penalty of applying the batches is spread across + * threads so that the amortized cost is slightly higher than performing just + * the ConcurrentHashMap operation. + * + * A memento of the reads and writes that were performed on the map are + * recorded in buffers. These buffers are drained at the first opportunity + * after a write or when the read buffer exceeds a threshold size. The reads + * are recorded in a lossy buffer, allowing the reordering operations to be + * discarded if the draining process cannot keep up. Due to the concurrent + * nature of the read and write operations a strict policy ordering is not + * possible, but is observably strict when single threaded. + * + * Due to a lack of a strict ordering guarantee, a task can be executed + * out-of-order, such as a removal followed by its addition. The state of the + * entry is encoded within the value's weight. + * + * Alive: The entry is in both the hash-table and the page replacement policy. + * This is represented by a positive weight. + * + * Retired: The entry is not in the hash-table and is pending removal from the + * page replacement policy. This is represented by a negative weight. + * + * Dead: The entry is not in the hash-table and is not in the page replacement + * policy. This is represented by a weight of zero. + * + * The Least Recently Used page replacement algorithm was chosen due to its + * simplicity, high hit rate, and ability to be implemented with O(1) time + * complexity. */ + /** The number of CPUs */ + static final int NCPU = Runtime.getRuntime().availableProcessors(); + /** The maximum weighted capacity of the map. */ - static final int MAXIMUM_CAPACITY = 1 << 30; + static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE; + + /** The number of read buffers to use. */ + static final int NUMBER_OF_READ_BUFFERS = ceilingNextPowerOfTwo(NCPU); - /** The maximum weight of a value. */ - static final int MAXIMUM_WEIGHT = 1 << 29; + /** Mask value for indexing into the read buffers. */ + static final int READ_BUFFERS_MASK = NUMBER_OF_READ_BUFFERS - 1; - /** The maximum number of pending operations per buffer. */ - static final int MAXIMUM_BUFFER_SIZE = 1 << 20; + /** The number of pending read operations before attempting to drain. */ + static final int READ_BUFFER_THRESHOLD = 32; - /** The number of pending operations per buffer before attempting to drain. */ - static final int BUFFER_THRESHOLD = 16; + /** The maximum number of read operations to perform per amortized drain. */ + static final int READ_BUFFER_DRAIN_THRESHOLD = 2 * READ_BUFFER_THRESHOLD; - /** The number of buffers to use. */ - static final int NUMBER_OF_BUFFERS; + /** The maximum number of pending reads per buffer. */ + static final int READ_BUFFER_SIZE = 2 * READ_BUFFER_DRAIN_THRESHOLD; - /** Mask value for indexing into the buffers. */ - static final int BUFFER_MASK; + /** Mask value for indexing into the read buffer. */ + static final int READ_BUFFER_INDEX_MASK = READ_BUFFER_SIZE - 1; - /** The maximum number of operations to perform per amortized drain. */ - static final int AMORTIZED_DRAIN_THRESHOLD; + /** The maximum number of write operations to perform per amortized drain. */ + static final int WRITE_BUFFER_DRAIN_THRESHOLD = 16; /** A queue that discards all entries. */ static final Queue DISCARDING_QUEUE = new DiscardingQueue(); - static { - int buffers = ceilingNextPowerOfTwo(Runtime.getRuntime().availableProcessors()); - AMORTIZED_DRAIN_THRESHOLD = (1 + buffers) * BUFFER_THRESHOLD; - NUMBER_OF_BUFFERS = buffers; - BUFFER_MASK = buffers - 1; - } - static int ceilingNextPowerOfTwo(int x) { // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)); } - /** The draining status of the buffers. */ - enum DrainStatus { - - /** A drain is not taking place. */ - IDLE, - - /** A drain is required due to a pending write modification. */ - REQUIRED, - - /** A drain is in progress. */ - PROCESSING - } - // The backing data store holding the key-value associations - final ConcurrentMap data; + final ConcurrentMap> data; final int concurrencyLevel; // These fields provide support to bound the map by a maximum capacity - final LinkedDeque evictionDeque; - - // must write under lock - volatile int weightedSize; + //@GuardedBy("evictionLock") + final long[] readBufferReadCount; + //@GuardedBy("evictionLock") + final LinkedDeque> evictionDeque; - // must write under lock - volatile int capacity; - - volatile int nextOrder; - int drainedOrder; + //@GuardedBy("evictionLock") // must write under lock + final AtomicLong weightedSize; + //@GuardedBy("evictionLock") // must write under lock + final AtomicLong capacity; final Lock evictionLock; - final Queue[] buffers; - final ExecutorService executor; - final Weigher weigher; - final AtomicIntegerArray bufferLengths; + final Queue writeBuffer; + final AtomicLong[] readBufferWriteCount; + final AtomicLong[] readBufferDrainAtWriteCount; + final AtomicReference>[][] readBuffers; + final AtomicReference drainStatus; + final EntryWeigher weigher; // These fields provide support for notifying a listener. - final Queue pendingNotifications; + final Queue> pendingNotifications; final EvictionListener listener; transient Set keySet; @@ -224,47 +209,62 @@ enum DrainStatus { /** * Creates an instance based on the builder's configuration. */ - @SuppressWarnings({ - "unchecked", "cast" - }) + @SuppressWarnings({"unchecked", "cast"}) private ConcurrentLinkedHashMap(Builder builder) { // The data store and its maximum capacity concurrencyLevel = builder.concurrencyLevel; - capacity = Math.min(builder.capacity, MAXIMUM_CAPACITY); - data = new ConcurrentHashMap<>( - builder.initialCapacity, - 0.75f, - concurrencyLevel); + capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); + data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); // The eviction support weigher = builder.weigher; - executor = builder.executor; - nextOrder = Integer.MIN_VALUE; - drainedOrder = Integer.MIN_VALUE; evictionLock = new ReentrantLock(); - evictionDeque = new LinkedDeque<>(); - drainStatus = new AtomicReference<>(DrainStatus.IDLE); - - buffers = (Queue[]) new Queue[NUMBER_OF_BUFFERS]; - bufferLengths = new AtomicIntegerArray(NUMBER_OF_BUFFERS); - for (int i = 0; i < NUMBER_OF_BUFFERS; i++) { - buffers[i] = new ConcurrentLinkedQueue<>(); + weightedSize = new AtomicLong(); + evictionDeque = new LinkedDeque>(); + writeBuffer = new ConcurrentLinkedQueue(); + drainStatus = new AtomicReference(IDLE); + + readBufferReadCount = new long[NUMBER_OF_READ_BUFFERS]; + readBufferWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; + readBufferDrainAtWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; + readBuffers = new AtomicReference[NUMBER_OF_READ_BUFFERS][READ_BUFFER_SIZE]; + for (int i = 0; i < NUMBER_OF_READ_BUFFERS; i++) { + readBufferWriteCount[i] = new AtomicLong(); + readBufferDrainAtWriteCount[i] = new AtomicLong(); + readBuffers[i] = new AtomicReference[READ_BUFFER_SIZE]; + for (int j = 0; j < READ_BUFFER_SIZE; j++) { + readBuffers[i][j] = new AtomicReference>(); + } } // The notification queue and listener listener = builder.listener; pendingNotifications = (listener == DiscardingListener.INSTANCE) - ? (Queue) DISCARDING_QUEUE - : new ConcurrentLinkedQueue(); + ? (Queue>) DISCARDING_QUEUE + : new ConcurrentLinkedQueue>(); } - /** Asserts that the object is not null. */ + /** Ensures that the object is not null. */ static void checkNotNull(Object o) { if (o == null) { throw new NullPointerException(); } } + /** Ensures that the argument expression is true. */ + static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + /** Ensures that the state expression is true. */ + static void checkState(boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } + } + /* ---------------- Eviction Support -------------- */ /** @@ -272,43 +272,41 @@ static void checkNotNull(Object o) { * * @return the maximum weighted capacity */ - public int capacity() { - return capacity; + public long capacity() { + return capacity.get(); } /** - * Sets the maximum weighted capacity of the map and eagerly evicts entries until it - * shrinks to the appropriate size. + * Sets the maximum weighted capacity of the map and eagerly evicts entries + * until it shrinks to the appropriate size. * * @param capacity the maximum weighted capacity of the map * @throws IllegalArgumentException if the capacity is negative */ - public void setCapacity(int capacity) { - if (capacity < 0) { - throw new IllegalArgumentException(); - } - + public void setCapacity(long capacity) { + checkArgument(capacity >= 0); evictionLock.lock(); try { - this.capacity = Math.min(capacity, MAXIMUM_CAPACITY); - drainBuffers(AMORTIZED_DRAIN_THRESHOLD); + this.capacity.lazySet(Math.min(capacity, MAXIMUM_CAPACITY)); + drainBuffers(); evict(); - } - finally { + } finally { evictionLock.unlock(); } notifyListener(); } /** Determines whether the map has exceeded its capacity. */ + //@GuardedBy("evictionLock") boolean hasOverflowed() { - return weightedSize > capacity; + return weightedSize.get() > capacity.get(); } /** - * Evicts entries from the map while it exceeds the capacity and appends evicted - * entries to the notification queue for processing. + * Evicts entries from the map while it exceeds the capacity and appends + * evicted entries to the notification queue for processing. */ + //@GuardedBy("evictionLock") void evict() { // Attempts to evict entries from the map if it exceeds the maximum // capacity. If the eviction fails due to a concurrent removal of the @@ -317,7 +315,7 @@ void evict() { // that if an eviction is still required then a new victim will be chosen // for removal. while (hasOverflowed()) { - Node node = evictionDeque.poll(); + final Node node = evictionDeque.poll(); // If weighted values are used, then the pending operations will adjust // the size to reflect the correct weight @@ -330,276 +328,230 @@ void evict() { pendingNotifications.add(node); } - node.makeDead(); + makeDead(node); } } /** - * Performs the post-processing work required after the map operation. + * Performs the post-processing work required after a read. * - * @param task the pending operation to be applied + * @param node the entry in the page replacement policy */ - void afterCompletion(Task task) { - boolean delayable = schedule(task); - if (shouldDrainBuffers(delayable)) { - tryToDrainBuffers(AMORTIZED_DRAIN_THRESHOLD); - } + void afterRead(Node node) { + final int bufferIndex = readBufferIndex(); + final long writeCount = recordRead(bufferIndex, node); + drainOnReadIfNeeded(bufferIndex, writeCount); notifyListener(); } + /** Returns the index to the read buffer to record into. */ + static int readBufferIndex() { + // A buffer is chosen by the thread's id so that tasks are distributed in a + // pseudo evenly manner. This helps avoid hot entries causing contention + // due to other threads trying to append to the same buffer. + return ((int) Thread.currentThread().getId()) & READ_BUFFERS_MASK; + } + /** - * Schedules the task to be applied to the page replacement policy. + * Records a read in the buffer and return its write count. * - * @param task the pending operation - * @return if the draining of the buffers can be delayed + * @param bufferIndex the index to the chosen read buffer + * @param node the entry in the page replacement policy + * @return the number of writes on the chosen read buffer */ - private boolean schedule(Task task) { - int index = bufferIndex(); - int buffered = bufferLengths.incrementAndGet(index); + long recordRead(int bufferIndex, Node node) { + // The location in the buffer is chosen in a racy fashion as the increment + // is not atomic with the insertion. This means that concurrent reads can + // overlap and overwrite one another, resulting in a lossy buffer. + final AtomicLong counter = readBufferWriteCount[bufferIndex]; + final long writeCount = counter.get(); + counter.lazySet(writeCount + 1); - if (task.isWrite()) { - buffers[index].add(task); - drainStatus.set(DrainStatus.REQUIRED); - return false; - } + final int index = (int) (writeCount & READ_BUFFER_INDEX_MASK); + readBuffers[bufferIndex][index].lazySet(node); - // A buffer may discard a read task if its length exceeds a tolerance level - if (buffered <= MAXIMUM_BUFFER_SIZE) { - buffers[index].add(task); - return (buffered <= BUFFER_THRESHOLD); - } - else { // not optimized for fail-safe scenario - bufferLengths.decrementAndGet(index); - return false; - } - } - - /** Returns the index to the buffer that the task should be scheduled on. */ - static int bufferIndex() { - // A buffer is chosen by the thread's id so that tasks are distributed in a - // pseudo evenly manner. This helps avoid hot entries causing contention due - // to other threads trying to append to the same buffer. - return (int) Thread.currentThread().getId() & BUFFER_MASK; - } - - /** Returns the ordering value to assign to a task. */ - int nextOrdering() { - // The next ordering is acquired in a racy fashion as the increment is not - // atomic with the insertion into a buffer. This means that concurrent tasks - // can have the same ordering and the buffers are in a weakly sorted order. - return nextOrder++; + return writeCount; } /** - * Determines whether the buffers should be drained. + * Attempts to drain the buffers if it is determined to be needed when + * post-processing a read. * - * @param delayable if a drain should be delayed until required - * @return if a drain should be attempted + * @param bufferIndex the index to the chosen read buffer + * @param writeCount the number of writes on the chosen read buffer */ - boolean shouldDrainBuffers(boolean delayable) { - if (executor.isShutdown()) { - DrainStatus status = drainStatus.get(); - return (status != DrainStatus.PROCESSING) - & (!delayable | (status == DrainStatus.REQUIRED)); + void drainOnReadIfNeeded(int bufferIndex, long writeCount) { + final long pending = (writeCount - readBufferDrainAtWriteCount[bufferIndex].get()); + final boolean delayable = (pending < READ_BUFFER_THRESHOLD); + final DrainStatus status = drainStatus.get(); + if (status.shouldDrainBuffers(delayable)) { + tryToDrainBuffers(); } - return false; } /** - * Attempts to acquire the eviction lock and apply the pending operations to the page - * replacement policy. + * Performs the post-processing work required after a write. * - * @param maxToDrain the maximum number of operations to drain + * @param task the pending operation to be applied */ - void tryToDrainBuffers(int maxToDrain) { + void afterWrite(Runnable task) { + writeBuffer.add(task); + drainStatus.lazySet(REQUIRED); + tryToDrainBuffers(); + notifyListener(); + } + + /** + * Attempts to acquire the eviction lock and apply the pending operations, up + * to the amortized threshold, to the page replacement policy. + */ + void tryToDrainBuffers() { if (evictionLock.tryLock()) { try { - drainStatus.set(DrainStatus.PROCESSING); - drainBuffers(maxToDrain); - } - finally { - drainStatus.compareAndSet(DrainStatus.PROCESSING, DrainStatus.IDLE); + drainStatus.lazySet(PROCESSING); + drainBuffers(); + } finally { + drainStatus.compareAndSet(PROCESSING, IDLE); evictionLock.unlock(); } } } - /** - * Drains the buffers and applies the pending operations. - * - * @param maxToDrain the maximum number of operations to drain - */ - void drainBuffers(int maxToDrain) { - // A mostly strict ordering is achieved by observing that each buffer - // contains tasks in a weakly sorted order starting from the last drain. - // The buffers can be merged into a sorted list in O(n) time by using - // counting sort and chaining on a collision. - - // The output is capped to the expected number of tasks plus additional - // slack to optimistically handle the concurrent additions to the buffers. - Task[] tasks = new Task[maxToDrain]; - - // Moves the tasks into the output array, applies them, and updates the - // marker for the starting order of the next drain. - int maxTaskIndex = moveTasksFromBuffers(tasks); - runTasks(tasks, maxTaskIndex); - updateDrainedOrder(tasks, maxTaskIndex); + /** Drains the read and write buffers up to an amortized threshold. */ + //@GuardedBy("evictionLock") + void drainBuffers() { + drainReadBuffers(); + drainWriteBuffer(); } - /** - * Moves the tasks from the buffers into the output array. - * - * @param tasks the ordered array of the pending operations - * @return the highest index location of a task that was added to the array - */ - int moveTasksFromBuffers(Task[] tasks) { - int maxTaskIndex = -1; - for (int i = 0; i < buffers.length; i++) { - int maxIndex = moveTasksFromBuffer(tasks, i); - maxTaskIndex = Math.max(maxIndex, maxTaskIndex); + /** Drains the read buffers, each up to an amortized threshold. */ + //@GuardedBy("evictionLock") + void drainReadBuffers() { + final int start = (int) Thread.currentThread().getId(); + final int end = start + NUMBER_OF_READ_BUFFERS; + for (int i = start; i < end; i++) { + drainReadBuffer(i & READ_BUFFERS_MASK); } - return maxTaskIndex; } - /** - * Moves the tasks from the specified buffer into the output array. - * - * @param tasks the ordered array of the pending operations - * @param bufferIndex the buffer to drain into the tasks array - * @return the highest index location of a task that was added to the array - */ - int moveTasksFromBuffer(Task[] tasks, int bufferIndex) { - // While a buffer is being drained it may be concurrently appended to. The - // number of tasks removed are tracked so that the length can be decremented - // by the delta rather than set to zero. - Queue buffer = buffers[bufferIndex]; - int removedFromBuffer = 0; - - Task task; - int maxIndex = -1; - while ((task = buffer.poll()) != null) { - removedFromBuffer++; - - // The index into the output array is determined by calculating the offset - // since the last drain - int index = task.getOrder() - drainedOrder; - if (index < 0) { - // The task was missed by the last drain and can be run immediately - task.run(); - } - else if (index >= tasks.length) { - // Due to concurrent additions, the order exceeds the capacity of the - // output array. It is added to the end as overflow and the remaining - // tasks in the buffer will be handled by the next drain. - maxIndex = tasks.length - 1; - addTaskToChain(tasks, task, maxIndex); + /** Drains the read buffer up to an amortized threshold. */ + //@GuardedBy("evictionLock") + void drainReadBuffer(int bufferIndex) { + final long writeCount = readBufferWriteCount[bufferIndex].get(); + for (int i = 0; i < READ_BUFFER_DRAIN_THRESHOLD; i++) { + final int index = (int) (readBufferReadCount[bufferIndex] & READ_BUFFER_INDEX_MASK); + final AtomicReference> slot = readBuffers[bufferIndex][index]; + final Node node = slot.get(); + if (node == null) { break; } - else { - maxIndex = Math.max(index, maxIndex); - addTaskToChain(tasks, task, index); - } + + slot.lazySet(null); + applyRead(node); + readBufferReadCount[bufferIndex]++; } - bufferLengths.addAndGet(bufferIndex, -removedFromBuffer); - return maxIndex; + readBufferDrainAtWriteCount[bufferIndex].lazySet(writeCount); } - /** - * Adds the task as the head of the chain at the index location. - * - * @param tasks the ordered array of the pending operations - * @param task the pending operation to add - * @param index the array location - */ - void addTaskToChain(Task[] tasks, Task task, int index) { - task.setNext(tasks[index]); - tasks[index] = task; + /** Updates the node's location in the page replacement policy. */ + //@GuardedBy("evictionLock") + void applyRead(Node node) { + // An entry may be scheduled for reordering despite having been removed. + // This can occur when the entry was concurrently read while a writer was + // removing it. If the entry is no longer linked then it does not need to + // be processed. + if (evictionDeque.contains(node)) { + evictionDeque.moveToBack(node); + } + } + + /** Drains the read buffer up to an amortized threshold. */ + //@GuardedBy("evictionLock") + void drainWriteBuffer() { + for (int i = 0; i < WRITE_BUFFER_DRAIN_THRESHOLD; i++) { + final Runnable task = writeBuffer.poll(); + if (task == null) { + break; + } + task.run(); + } } /** - * Runs the pending page replacement policy operations. + * Attempts to transition the node from the alive state to the + * retired state. * - * @param tasks the ordered array of the pending operations - * @param maxTaskIndex the maximum index of the array + * @param node the entry in the page replacement policy + * @param expect the expected weighted value + * @return if successful */ - void runTasks(Task[] tasks, int maxTaskIndex) { - for (int i = 0; i <= maxTaskIndex; i++) { - runTasksInChain(tasks[i]); + boolean tryToRetire(Node node, WeightedValue expect) { + if (expect.isAlive()) { + final WeightedValue retired = new WeightedValue(expect.value, -expect.weight); + return node.compareAndSet(expect, retired); } + return false; } /** - * Runs the pending operations on the linked chain. + * Atomically transitions the node from the alive state to the + * retired state, if a valid transition. * - * @param task the first task in the chain of operations + * @param node the entry in the page replacement policy */ - void runTasksInChain(Task task) { - while (task != null) { - Task current = task; - task = task.getNext(); - current.setNext(null); - current.run(); + void makeRetired(Node node) { + for (;;) { + final WeightedValue current = node.get(); + if (!current.isAlive()) { + return; + } + final WeightedValue retired = new WeightedValue(current.value, -current.weight); + if (node.compareAndSet(current, retired)) { + return; + } } } /** - * Updates the order to start the next drain from. + * Atomically transitions the node to the dead state and decrements + * the weightedSize. * - * @param tasks the ordered array of operations - * @param maxTaskIndex the maximum index of the array + * @param node the entry in the page replacement policy */ - void updateDrainedOrder(Task[] tasks, int maxTaskIndex) { - if (maxTaskIndex >= 0) { - Task task = tasks[maxTaskIndex]; - drainedOrder = task.getOrder() + 1; + //@GuardedBy("evictionLock") + void makeDead(Node node) { + for (;;) { + WeightedValue current = node.get(); + WeightedValue dead = new WeightedValue(current.value, 0); + if (node.compareAndSet(current, dead)) { + weightedSize.lazySet(weightedSize.get() - Math.abs(current.weight)); + return; + } } } /** Notifies the listener of entries that were evicted. */ void notifyListener() { - Node node; + Node node; while ((node = pendingNotifications.poll()) != null) { listener.onEviction(node.key, node.getValue()); } } - /** Updates the node's location in the page replacement policy. */ - class ReadTask extends AbstractTask { - - final Node node; - - ReadTask(Node node) { - this.node = node; - } - - public void run() { - // An entry may scheduled for reordering despite having been previously - // removed. This can occur when the entry was concurrently read while a - // writer was removing it. If the entry is no longer linked then it does - // not need to be processed. - if (evictionDeque.contains(node)) { - evictionDeque.moveToBack(node); - } - } - - public boolean isWrite() { - return false; - } - } - /** Adds the node to the page replacement policy. */ - final class AddTask extends AbstractTask { - - final Node node; + final class AddTask implements Runnable { + final Node node; final int weight; - AddTask(Node node, int weight) { + AddTask(Node node, int weight) { this.weight = weight; this.node = node; } + @Override + //@GuardedBy("evictionLock") public void run() { - weightedSize += weight; + weightedSize.lazySet(weightedSize.get() + weight); // ignore out-of-order write operations if (node.get().isAlive()) { @@ -607,53 +559,42 @@ public void run() { evict(); } } - - public boolean isWrite() { - return true; - } } /** Removes a node from the page replacement policy. */ - final class RemovalTask extends AbstractTask { - - final Node node; + final class RemovalTask implements Runnable { + final Node node; - RemovalTask(Node node) { + RemovalTask(Node node) { this.node = node; } + @Override + //@GuardedBy("evictionLock") public void run() { // add may not have been processed yet evictionDeque.remove(node); - node.makeDead(); - } - - public boolean isWrite() { - return true; + makeDead(node); } } /** Updates the weighted size and evicts an entry on overflow. */ - final class UpdateTask extends ReadTask { - + final class UpdateTask implements Runnable { final int weightDifference; + final Node node; - public UpdateTask(Node node, int weightDifference) { - super(node); + public UpdateTask(Node node, int weightDifference) { this.weightDifference = weightDifference; + this.node = node; } @Override + //@GuardedBy("evictionLock") public void run() { - super.run(); - weightedSize += weightDifference; + weightedSize.lazySet(weightedSize.get() + weightDifference); + applyRead(node); evict(); } - - @Override - public boolean isWrite() { - return true; - } } /* ---------------- Concurrent Map Support -------------- */ @@ -673,37 +614,34 @@ public int size() { * * @return the combined weight of the values in this map */ - public int weightedSize() { - return Math.max(0, weightedSize); + public long weightedSize() { + return Math.max(0, weightedSize.get()); } @Override public void clear() { - // The alternative is to iterate through the keys and call #remove(), which - // adds unnecessary contention on the eviction lock and buffers. evictionLock.lock(); try { - Node node; + // Discard all entries + Node node; while ((node = evictionDeque.poll()) != null) { data.remove(node.key, node); - node.makeDead(); + makeDead(node); } - // Drain the buffers and run only the write tasks - for (int i = 0; i < buffers.length; i++) { - Queue buffer = buffers[i]; - int removed = 0; - Task task; - while ((task = buffer.poll()) != null) { - if (task.isWrite()) { - task.run(); - } - removed++; + // Discard all pending reads + for (AtomicReference>[] buffer : readBuffers) { + for (AtomicReference> slot : buffer) { + slot.lazySet(null); } - bufferLengths.addAndGet(i, -removed); } - } - finally { + + // Apply all pending writes + Runnable task; + while ((task = writeBuffer.poll()) != null) { + task.run(); + } + } finally { evictionLock.unlock(); } } @@ -717,7 +655,7 @@ public boolean containsKey(Object key) { public boolean containsValue(Object value) { checkNotNull(value); - for (Node node : data.values()) { + for (Node node : data.values()) { if (node.getValue().equals(value)) { return true; } @@ -727,48 +665,65 @@ public boolean containsValue(Object value) { @Override public V get(Object key) { - final Node node = data.get(key); + final Node node = data.get(key); if (node == null) { return null; } - afterCompletion(new ReadTask(node)); + afterRead(node); return node.getValue(); } + /** + * Returns the value to which the specified key is mapped, or {@code null} + * if this map contains no mapping for the key. This method differs from + * {@link #get(Object)} in that it does not record the operation with the + * page replacement policy. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key + * @throws NullPointerException if the specified key is null + */ + public V getQuietly(Object key) { + final Node node = data.get(key); + return (node == null) ? null : node.getValue(); + } + @Override public V put(K key, V value) { return put(key, value, false); } + @Override public V putIfAbsent(K key, V value) { return put(key, value, true); } /** - * Adds a node to the list and the data store. If an existing node is found, then its - * value is updated if allowed. + * Adds a node to the list and the data store. If an existing node is found, + * then its value is updated if allowed. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key - * @param onlyIfAbsent a write is performed only if the key is not already associated - * with a value + * @param onlyIfAbsent a write is performed only if the key is not already + * associated with a value * @return the prior value in the data store or null if no mapping was found */ V put(K key, V value, boolean onlyIfAbsent) { + checkNotNull(key); checkNotNull(value); - final int weight = weigher.weightOf(value); - final WeightedValue weightedValue = new WeightedValue<>(value, weight); - final Node node = new Node(key, weightedValue); + final int weight = weigher.weightOf(key, value); + final WeightedValue weightedValue = new WeightedValue(value, weight); + final Node node = new Node(key, weightedValue); for (;;) { - final Node prior = data.putIfAbsent(node.key, node); + final Node prior = data.putIfAbsent(node.key, node); if (prior == null) { - afterCompletion(new AddTask(node, weight)); + afterWrite(new AddTask(node, weight)); return null; - } - else if (onlyIfAbsent) { - afterCompletion(new ReadTask(prior)); + } else if (onlyIfAbsent) { + afterRead(prior); return prior.getValue(); } for (;;) { @@ -779,10 +734,11 @@ else if (onlyIfAbsent) { if (prior.compareAndSet(oldWeightedValue, weightedValue)) { final int weightedDifference = weight - oldWeightedValue.weight; - final Task task = (weightedDifference == 0) - ? new ReadTask(prior) - : new UpdateTask(prior, weightedDifference); - afterCompletion(task); + if (weightedDifference == 0) { + afterRead(prior); + } else { + afterWrite(new UpdateTask(prior, weightedDifference)); + } return oldWeightedValue.value; } } @@ -791,36 +747,35 @@ else if (onlyIfAbsent) { @Override public V remove(Object key) { - final Node node = data.remove(key); + final Node node = data.remove(key); if (node == null) { return null; } - node.makeRetired(); - afterCompletion(new RemovalTask(node)); + makeRetired(node); + afterWrite(new RemovalTask(node)); return node.getValue(); } + @Override public boolean remove(Object key, Object value) { - Node node = data.get(key); + final Node node = data.get(key); if ((node == null) || (value == null)) { return false; } WeightedValue weightedValue = node.get(); for (;;) { - if (weightedValue.hasValue(value)) { - if (node.tryToRetire(weightedValue)) { + if (weightedValue.contains(value)) { + if (tryToRetire(node, weightedValue)) { if (data.remove(key, node)) { - afterCompletion(new RemovalTask(node)); + afterWrite(new RemovalTask(node)); return true; } - } - else { + } else { weightedValue = node.get(); if (weightedValue.isAlive()) { - // retry as an intermediate update may have replaced the value - // with + // retry as an intermediate update may have replaced the value with // an equal instance that has a different reference identity continue; } @@ -830,54 +785,60 @@ public boolean remove(Object key, Object value) { } } + @Override public V replace(K key, V value) { + checkNotNull(key); checkNotNull(value); - final int weight = weigher.weightOf(value); - final WeightedValue weightedValue = new WeightedValue<>(value, weight); + final int weight = weigher.weightOf(key, value); + final WeightedValue weightedValue = new WeightedValue(value, weight); - final Node node = data.get(key); + final Node node = data.get(key); if (node == null) { return null; } for (;;) { - WeightedValue oldWeightedValue = node.get(); + final WeightedValue oldWeightedValue = node.get(); if (!oldWeightedValue.isAlive()) { return null; } if (node.compareAndSet(oldWeightedValue, weightedValue)) { - int weightedDifference = weight - oldWeightedValue.weight; - final Task task = (weightedDifference == 0) - ? new ReadTask(node) - : new UpdateTask(node, weightedDifference); - afterCompletion(task); + final int weightedDifference = weight - oldWeightedValue.weight; + if (weightedDifference == 0) { + afterRead(node); + } else { + afterWrite(new UpdateTask(node, weightedDifference)); + } return oldWeightedValue.value; } } } + @Override public boolean replace(K key, V oldValue, V newValue) { + checkNotNull(key); checkNotNull(oldValue); checkNotNull(newValue); - final int weight = weigher.weightOf(newValue); - final WeightedValue newWeightedValue = new WeightedValue<>(newValue, weight); + final int weight = weigher.weightOf(key, newValue); + final WeightedValue newWeightedValue = new WeightedValue(newValue, weight); - final Node node = data.get(key); + final Node node = data.get(key); if (node == null) { return false; } for (;;) { final WeightedValue weightedValue = node.get(); - if (!weightedValue.isAlive() || !weightedValue.hasValue(oldValue)) { + if (!weightedValue.isAlive() || !weightedValue.contains(oldValue)) { return false; } if (node.compareAndSet(weightedValue, newWeightedValue)) { - int weightedDifference = weight - weightedValue.weight; - final Task task = (weightedDifference == 0) - ? new ReadTask(node) - : new UpdateTask(node, weightedDifference); - afterCompletion(task); + final int weightedDifference = weight - weightedValue.weight; + if (weightedDifference == 0) { + afterRead(node); + } else { + afterWrite(new UpdateTask(node, weightedDifference)); + } return true; } } @@ -885,35 +846,37 @@ public boolean replace(K key, V oldValue, V newValue) { @Override public Set keySet() { - Set ks = keySet; + final Set ks = keySet; return (ks == null) ? (keySet = new KeySet()) : ks; } /** - * Returns a unmodifiable snapshot {@link Set} view of the keys contained in this map. - * The set's iterator returns the keys whose order of iteration is the ascending order - * in which its entries are considered eligible for retention, from the least-likely - * to be retained to the most-likely. + * Returns a unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the ascending order in which its entries are considered eligible for + * retention, from the least-likely to be retained to the most-likely. *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a - * constant-time operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the keys. + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. * * @return an ascending snapshot view of the keys in this map */ public Set ascendingKeySet() { - return orderedKeySet(true, Integer.MAX_VALUE); + return ascendingKeySetWithLimit(Integer.MAX_VALUE); } /** - * Returns an unmodifiable snapshot {@link Set} view of the keys contained in this - * map. The set's iterator returns the keys whose order of iteration is the ascending - * order in which its entries are considered eligible for retention, from the - * least-likely to be retained to the most-likely. + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the ascending order in which its entries are considered eligible for + * retention, from the least-likely to be retained to the most-likely. *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a - * constant-time operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the keys. + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. * * @param limit the maximum size of the returned set * @return a ascending snapshot view of the keys in this map @@ -924,30 +887,32 @@ public Set ascendingKeySetWithLimit(int limit) { } /** - * Returns an unmodifiable snapshot {@link Set} view of the keys contained in this - * map. The set's iterator returns the keys whose order of iteration is the descending - * order in which its entries are considered eligible for retention, from the - * most-likely to be retained to the least-likely. + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the descending order in which its entries are considered eligible for + * retention, from the most-likely to be retained to the least-likely. *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a - * constant-time operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the keys. + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. * * @return a descending snapshot view of the keys in this map */ public Set descendingKeySet() { - return orderedKeySet(false, Integer.MAX_VALUE); + return descendingKeySetWithLimit(Integer.MAX_VALUE); } /** - * Returns an unmodifiable snapshot {@link Set} view of the keys contained in this - * map. The set's iterator returns the keys whose order of iteration is the descending - * order in which its entries are considered eligible for retention, from the - * most-likely to be retained to the least-likely. + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the descending order in which its entries are considered eligible for + * retention, from the most-likely to be retained to the least-likely. *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a - * constant-time operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the keys. + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. * * @param limit the maximum size of the returned set * @return a descending snapshot view of the keys in this map @@ -958,67 +923,68 @@ public Set descendingKeySetWithLimit(int limit) { } Set orderedKeySet(boolean ascending, int limit) { - if (limit < 0) { - throw new IllegalArgumentException(); - } + checkArgument(limit >= 0); evictionLock.lock(); try { - drainBuffers(AMORTIZED_DRAIN_THRESHOLD); + drainBuffers(); - int initialCapacity = (weigher == Weighers.singleton()) ? Math.min( - limit, - weightedSize()) : 16; - Set keys = new LinkedHashSet<>(initialCapacity); - Iterator iterator = ascending + final int initialCapacity = (weigher == Weighers.entrySingleton()) + ? Math.min(limit, (int) weightedSize()) + : 16; + final Set keys = new LinkedHashSet(initialCapacity); + final Iterator> iterator = ascending ? evictionDeque.iterator() : evictionDeque.descendingIterator(); while (iterator.hasNext() && (limit > keys.size())) { keys.add(iterator.next().key); } - return Collections.unmodifiableSet(keys); - } - finally { + return unmodifiableSet(keys); + } finally { evictionLock.unlock(); } } @Override public Collection values() { - Collection vs = values; + final Collection vs = values; return (vs == null) ? (values = new Values()) : vs; } @Override public Set> entrySet() { - Set> es = entrySet; + final Set> es = entrySet; return (es == null) ? (entrySet = new EntrySet()) : es; } /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this - * map. The map's collections return the mappings whose order of iteration is the - * ascending order in which its entries are considered eligible for retention, from - * the least-likely to be retained to the most-likely. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the ascending order in which its entries are considered + * eligible for retention, from the least-likely to be retained to the + * most-likely. *

- * Beware that obtaining the mappings is NOT a constant-time operation. - * Because of the asynchronous nature of the page replacement policy, determining the - * retention ordering requires a traversal of the entries. + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. * * @return a ascending snapshot view of this map */ public Map ascendingMap() { - return orderedMap(true, Integer.MAX_VALUE); + return ascendingMapWithLimit(Integer.MAX_VALUE); } /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this - * map. The map's collections return the mappings whose order of iteration is the - * ascending order in which its entries are considered eligible for retention, from - * the least-likely to be retained to the most-likely. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the ascending order in which its entries are considered + * eligible for retention, from the least-likely to be retained to the + * most-likely. *

- * Beware that obtaining the mappings is NOT a constant-time operation. - * Because of the asynchronous nature of the page replacement policy, determining the - * retention ordering requires a traversal of the entries. + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. * * @param limit the maximum size of the returned map * @return a ascending snapshot view of this map @@ -1029,30 +995,34 @@ public Map ascendingMapWithLimit(int limit) { } /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this - * map. The map's collections return the mappings whose order of iteration is the - * descending order in which its entries are considered eligible for retention, from - * the most-likely to be retained to the least-likely. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the descending order in which its entries are considered + * eligible for retention, from the most-likely to be retained to the + * least-likely. *

- * Beware that obtaining the mappings is NOT a constant-time operation. - * Because of the asynchronous nature of the page replacement policy, determining the - * retention ordering requires a traversal of the entries. + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. * * @return a descending snapshot view of this map */ public Map descendingMap() { - return orderedMap(false, Integer.MAX_VALUE); + return descendingMapWithLimit(Integer.MAX_VALUE); } /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this - * map. The map's collections return the mappings whose order of iteration is the - * descending order in which its entries are considered eligible for retention, from - * the most-likely to be retained to the least-likely. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the descending order in which its entries are considered + * eligible for retention, from the most-likely to be retained to the + * least-likely. *

- * Beware that obtaining the mappings is NOT a constant-time operation. - * Because of the asynchronous nature of the page replacement policy, determining the - * retention ordering requires a traversal of the entries. + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. * * @param limit the maximum size of the returned map * @return a descending snapshot view of this map @@ -1063,34 +1033,64 @@ public Map descendingMapWithLimit(int limit) { } Map orderedMap(boolean ascending, int limit) { - if (limit < 0) { - throw new IllegalArgumentException(); - } + checkArgument(limit >= 0); evictionLock.lock(); try { - drainBuffers(AMORTIZED_DRAIN_THRESHOLD); + drainBuffers(); - int initialCapacity = (weigher == Weighers.singleton()) ? Math.min( - limit, - weightedSize()) : 16; - Map map = new LinkedHashMap<>(initialCapacity); - Iterator iterator = ascending + final int initialCapacity = (weigher == Weighers.entrySingleton()) + ? Math.min(limit, (int) weightedSize()) + : 16; + final Map map = new LinkedHashMap(initialCapacity); + final Iterator> iterator = ascending ? evictionDeque.iterator() : evictionDeque.descendingIterator(); while (iterator.hasNext() && (limit > map.size())) { - Node node = iterator.next(); + Node node = iterator.next(); map.put(node.key, node.getValue()); } - return Collections.unmodifiableMap(map); - } - finally { + return unmodifiableMap(map); + } finally { evictionLock.unlock(); } } + /** The draining status of the buffers. */ + enum DrainStatus { + + /** A drain is not taking place. */ + IDLE { + @Override boolean shouldDrainBuffers(boolean delayable) { + return !delayable; + } + }, + + /** A drain is required due to a pending write modification. */ + REQUIRED { + @Override boolean shouldDrainBuffers(boolean delayable) { + return true; + } + }, + + /** A drain is in progress. */ + PROCESSING { + @Override boolean shouldDrainBuffers(boolean delayable) { + return false; + } + }; + + /** + * Determines whether the buffers should be drained. + * + * @param delayable if a drain should be delayed until required + * @return if a drain should be attempted + */ + abstract boolean shouldDrainBuffers(boolean delayable); + } + /** A value, its weight, and the entry's status. */ + //@Immutable static final class WeightedValue { - final int weight; final V value; @@ -1099,7 +1099,7 @@ static final class WeightedValue { this.value = value; } - boolean hasValue(Object o) { + boolean contains(Object o) { return (o == value) || value.equals(o); } @@ -1111,15 +1111,16 @@ boolean isAlive() { } /** - * If the entry was removed from the hash-table and is awaiting removal from the - * page replacement policy. + * If the entry was removed from the hash-table and is awaiting removal from + * the page replacement policy. */ boolean isRetired() { return weight < 0; } /** - * If the entry was removed from the hash-table and the page replacement policy. + * If the entry was removed from the hash-table and the page replacement + * policy. */ boolean isDead() { return weight == 0; @@ -1127,16 +1128,17 @@ boolean isDead() { } /** - * A node contains the key, the weighted value, and the linkage pointers on the - * page-replacement algorithm's data structures. + * A node contains the key, the weighted value, and the linkage pointers on + * the page-replacement algorithm's data structures. */ @SuppressWarnings("serial") - final class Node extends AtomicReference> implements Linked { - + static final class Node extends AtomicReference> + implements Linked> { final K key; - - Node prev; - Node next; + //@GuardedBy("evictionLock") + Node prev; + //@GuardedBy("evictionLock") + Node next; /** Creates a new, unlinked node. */ Node(K key, WeightedValue weightedValue) { @@ -1144,19 +1146,27 @@ final class Node extends AtomicReference> implements Linked getPrevious() { return prev; } - public void setPrevious(Node prev) { + @Override + //@GuardedBy("evictionLock") + public void setPrevious(Node prev) { this.prev = prev; } - public Node getNext() { + @Override + //@GuardedBy("evictionLock") + public Node getNext() { return next; } - public void setNext(Node next) { + @Override + //@GuardedBy("evictionLock") + public void setNext(Node next) { this.next = next; } @@ -1164,62 +1174,10 @@ public void setNext(Node next) { V getValue() { return get().value; } - - /** - * Attempts to transition the node from the alive state to the - * retired state. - * - * @param expect the expected weighted value - * @return if successful - */ - boolean tryToRetire(WeightedValue expect) { - if (expect.isAlive()) { - WeightedValue retired = new WeightedValue<>( - expect.value, - -expect.weight); - return compareAndSet(expect, retired); - } - return false; - } - - /** - * Atomically transitions the node from the alive state to the - * retired state, if a valid transition. - */ - void makeRetired() { - for (;;) { - WeightedValue current = get(); - if (!current.isAlive()) { - return; - } - WeightedValue retired = new WeightedValue<>( - current.value, - -current.weight); - if (compareAndSet(current, retired)) { - return; - } - } - } - - /** - * Atomically transitions the node to the dead state and decrements the - * weightedSize. - */ - void makeDead() { - for (;;) { - WeightedValue current = get(); - WeightedValue dead = new WeightedValue<>(current.value, 0); - if (compareAndSet(current, dead)) { - weightedSize -= Math.abs(current.weight); - return; - } - } - } } /** An adapter to safely externalize the keys. */ final class KeySet extends AbstractSet { - final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; @Override @@ -1260,23 +1218,23 @@ public T[] toArray(T[] array) { /** An adapter to safely externalize the key iterator. */ final class KeyIterator implements Iterator { - final Iterator iterator = data.keySet().iterator(); K current; + @Override public boolean hasNext() { return iterator.hasNext(); } + @Override public K next() { current = iterator.next(); return current; } + @Override public void remove() { - if (current == null) { - throw new IllegalStateException(); - } + checkState(current != null); ConcurrentLinkedHashMap.this.remove(current); current = null; } @@ -1308,23 +1266,23 @@ public boolean contains(Object o) { /** An adapter to safely externalize the value iterator. */ final class ValueIterator implements Iterator { + final Iterator> iterator = data.values().iterator(); + Node current; - final Iterator iterator = data.values().iterator(); - Node current; - + @Override public boolean hasNext() { return iterator.hasNext(); } + @Override public V next() { current = iterator.next(); return current.getValue(); } + @Override public void remove() { - if (current == null) { - throw new IllegalStateException(); - } + checkState(current != null); ConcurrentLinkedHashMap.this.remove(current.key); current = null; } @@ -1332,7 +1290,6 @@ public void remove() { /** An adapter to safely externalize the entries. */ final class EntrySet extends AbstractSet> { - final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; @Override @@ -1356,7 +1313,7 @@ public boolean contains(Object obj) { return false; } Entry entry = (Entry) obj; - Node node = map.data.get(entry.getKey()); + Node node = map.data.get(entry.getKey()); return (node != null) && (node.getValue().equals(entry.getValue())); } @@ -1377,34 +1334,33 @@ public boolean remove(Object obj) { /** An adapter to safely externalize the entry iterator. */ final class EntryIterator implements Iterator> { + final Iterator> iterator = data.values().iterator(); + Node current; - final Iterator iterator = data.values().iterator(); - Node current; - + @Override public boolean hasNext() { return iterator.hasNext(); } + @Override public Entry next() { current = iterator.next(); return new WriteThroughEntry(current); } + @Override public void remove() { - if (current == null) { - throw new IllegalStateException(); - } + checkState(current != null); ConcurrentLinkedHashMap.this.remove(current.key); current = null; } } /** An entry that allows updates to write through to the map. */ - final class WriteThroughEntry extends AbstractMap.SimpleEntry { - + final class WriteThroughEntry extends SimpleEntry { static final long serialVersionUID = 1; - WriteThroughEntry(Node node) { + WriteThroughEntry(Node node) { super(node.key, node.getValue()); } @@ -1415,26 +1371,24 @@ public V setValue(V value) { } Object writeReplace() { - return new AbstractMap.SimpleEntry<>(this); + return new SimpleEntry(this); } } /** A weigher that enforces that the weight falls within a valid range. */ - static final class BoundedWeigher implements Weigher, Serializable { - + static final class BoundedEntryWeigher implements EntryWeigher, Serializable { static final long serialVersionUID = 1; - final Weigher weigher; + final EntryWeigher weigher; - BoundedWeigher(Weigher weigher) { + BoundedEntryWeigher(EntryWeigher weigher) { checkNotNull(weigher); this.weigher = weigher; } - public int weightOf(V value) { - int weight = weigher.weightOf(value); - if ((weight < 1) || (weight > MAXIMUM_WEIGHT)) { - throw new IllegalArgumentException("invalid weight"); - } + @Override + public int weightOf(K key, V value) { + int weight = weigher.weightOf(key, value); + checkArgument(weight >= 1); return weight; } @@ -1443,133 +1397,21 @@ Object writeReplace() { } } - /** A task that catches up the page replacement policy. */ - static final class CatchUpTask implements Runnable { - - final WeakReference> mapRef; - - CatchUpTask(ConcurrentLinkedHashMap map) { - this.mapRef = new WeakReference>(map); - } - - public void run() { - ConcurrentLinkedHashMap map = mapRef.get(); - if (map == null) { - throw new CancellationException(); - } - int pendingTasks = 0; - for (int i = 0; i < map.buffers.length; i++) { - pendingTasks += map.bufferLengths.get(i); - } - if (pendingTasks != 0) { - map.tryToDrainBuffers(pendingTasks + BUFFER_THRESHOLD); - } - } - } - - /** An executor that is always terminated. */ - static final class DisabledExecutorService extends AbstractExecutorService { - - public boolean isShutdown() { - return true; - } - - public boolean isTerminated() { - return true; - } - - public void shutdown() { - } - - public List shutdownNow() { - return Collections.emptyList(); - } - - public boolean awaitTermination(long timeout, TimeUnit unit) { - return true; - } - - public void execute(Runnable command) { - throw new RejectedExecutionException(); - } - } - /** A queue that discards all additions and is always empty. */ static final class DiscardingQueue extends AbstractQueue { - - @Override - public boolean add(Object e) { - return true; - } - - public boolean offer(Object e) { - return true; - } - - public Object poll() { - return null; - } - - public Object peek() { - return null; - } - - @Override - public int size() { - return 0; - } - - @Override - public Iterator iterator() { - return Collections.emptyList().iterator(); - } + @Override public boolean add(Object e) { return true; } + @Override public boolean offer(Object e) { return true; } + @Override public Object poll() { return null; } + @Override public Object peek() { return null; } + @Override public int size() { return 0; } + @Override public Iterator iterator() { return emptyList().iterator(); } } /** A listener that ignores all notifications. */ enum DiscardingListener implements EvictionListener { INSTANCE; - public void onEviction(Object key, Object value) { - } - } - - /** An operation that can be lazily applied to the page replacement policy. */ - interface Task extends Runnable { - - /** The priority order. */ - int getOrder(); - - /** If the task represents an add, modify, or remove operation. */ - boolean isWrite(); - - /** Returns the next task on the link chain. */ - Task getNext(); - - /** Sets the next task on the link chain. */ - void setNext(Task task); - } - - /** A skeletal implementation of the Task interface. */ - abstract class AbstractTask implements Task { - - final int order; - Task task; - - AbstractTask() { - order = nextOrdering(); - } - - public int getOrder() { - return order; - } - - public Task getNext() { - return task; - } - - public void setNext(Task task) { - this.task = task; - } + @Override public void onEviction(Object key, Object value) {} } /* ---------------- Serialization Support -------------- */ @@ -1577,7 +1419,7 @@ public void setNext(Task task) { static final long serialVersionUID = 1; Object writeReplace() { - return new SerializationProxy<>(this); + return new SerializationProxy(this); } private void readObject(ObjectInputStream stream) throws InvalidObjectException { @@ -1585,23 +1427,23 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException } /** - * A proxy that is serialized instead of the map. The page-replacement algorithm's - * data structures are not serialized so the deserialized instance contains only the - * entries. This is acceptable as caches hold transient data that is recomputable and - * serialization would tend to be used as a fast warm-up process. + * A proxy that is serialized instead of the map. The page-replacement + * algorithm's data structures are not serialized so the deserialized + * instance contains only the entries. This is acceptable as caches hold + * transient data that is recomputable and serialization would tend to be + * used as a fast warm-up process. */ static final class SerializationProxy implements Serializable { - + final EntryWeigher weigher; final EvictionListener listener; - final Weigher weigher; final int concurrencyLevel; final Map data; - final int capacity; + final long capacity; SerializationProxy(ConcurrentLinkedHashMap map) { concurrencyLevel = map.concurrencyLevel; - data = new HashMap<>(map); - capacity = map.capacity; + data = new HashMap(map); + capacity = map.capacity.get(); listener = map.listener; weigher = map.weigher; } @@ -1623,136 +1465,84 @@ Object readResolve() { /* ---------------- Builder -------------- */ /** - * A builder that creates {@link ConcurrentLinkedHashMap} instances. It provides a - * flexible approach for constructing customized instances with a named parameter - * syntax. It can be used in the following manner: - * - *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     *
-     * {
-     *     @code
-     *     ConcurrentMap<Vertex, Set<Edge>> graph = new Builder<Vertex, Set<Edge>>()
-     *             .maximumWeightedCapacity(5000)
-     *             .weigher(Weighers.<Edge> set())
-     *             .build();
-     * }
-     * 
+ * A builder that creates {@link ConcurrentLinkedHashMap} instances. It + * provides a flexible approach for constructing customized instances with + * a named parameter syntax. It can be used in the following manner: + *
{@code
+     * ConcurrentMap> graph = new Builder>()
+     *     .maximumWeightedCapacity(5000)
+     *     .weigher(Weighers.set())
+     *     .build();
+     * }
*/ public static final class Builder { - - static final ExecutorService DEFAULT_EXECUTOR = new DisabledExecutorService(); static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final int DEFAULT_INITIAL_CAPACITY = 16; EvictionListener listener; - Weigher weigher; - - ExecutorService executor; - TimeUnit unit; - long delay; + EntryWeigher weigher; int concurrencyLevel; int initialCapacity; - int capacity; + long capacity; @SuppressWarnings("unchecked") public Builder() { capacity = -1; - executor = DEFAULT_EXECUTOR; - weigher = Weighers.singleton(); + weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; listener = (EvictionListener) DiscardingListener.INSTANCE; } /** - * Specifies the initial capacity of the hash table (default 16). This is - * the number of key-value pairs that the hash table can hold before a resize - * operation is required. + * Specifies the initial capacity of the hash table (default 16). + * This is the number of key-value pairs that the hash table can hold + * before a resize operation is required. * - * @param initialCapacity the initial capacity used to size the hash table to - * accommodate this many entries. + * @param initialCapacity the initial capacity used to size the hash table + * to accommodate this many entries. * @throws IllegalArgumentException if the initialCapacity is negative */ public Builder initialCapacity(int initialCapacity) { - if (initialCapacity < 0) { - throw new IllegalArgumentException(); - } + checkArgument(initialCapacity >= 0); this.initialCapacity = initialCapacity; return this; } /** - * Specifies the maximum weighted capacity to coerce the map to and may exceed it - * temporarily. + * Specifies the maximum weighted capacity to coerce the map to and may + * exceed it temporarily. * * @param capacity the weighted threshold to bound the map by - * @throws IllegalArgumentException if the maximumWeightedCapacity is negative + * @throws IllegalArgumentException if the maximumWeightedCapacity is + * negative */ - public Builder maximumWeightedCapacity(int capacity) { - if (capacity < 0) { - throw new IllegalArgumentException(); - } + public Builder maximumWeightedCapacity(long capacity) { + checkArgument(capacity >= 0); this.capacity = capacity; return this; } /** * Specifies the estimated number of concurrently updating threads. The - * implementation performs internal sizing to try to accommodate this many threads - * (default 16). + * implementation performs internal sizing to try to accommodate this many + * threads (default 16). * - * @param concurrencyLevel the estimated number of concurrently updating threads - * @throws IllegalArgumentException if the concurrencyLevel is less than or equal - * to zero + * @param concurrencyLevel the estimated number of concurrently updating + * threads + * @throws IllegalArgumentException if the concurrencyLevel is less than or + * equal to zero */ public Builder concurrencyLevel(int concurrencyLevel) { - if (concurrencyLevel <= 0) { - throw new IllegalArgumentException(); - } + checkArgument(concurrencyLevel > 0); this.concurrencyLevel = concurrencyLevel; return this; } /** - * Specifies an optional listener that is registered for notification when an - * entry is evicted. + * Specifies an optional listener that is registered for notification when + * an entry is evicted. * * @param listener the object to forward evicted entries to * @throws NullPointerException if the listener is null @@ -1764,71 +1554,44 @@ public Builder listener(EvictionListener listener) { } /** - * Specifies an algorithm to determine how many the units of capacity a value - * consumes. The default algorithm bounds the map by the number of key-value pairs - * by giving each entry a weight of 1. + * Specifies an algorithm to determine how many the units of capacity a + * value consumes. The default algorithm bounds the map by the number of + * key-value pairs by giving each entry a weight of 1. * * @param weigher the algorithm to determine a value's weight * @throws NullPointerException if the weigher is null */ public Builder weigher(Weigher weigher) { this.weigher = (weigher == Weighers.singleton()) - ? Weighers. singleton() - : new BoundedWeigher<>(weigher); + ? Weighers.entrySingleton() + : new BoundedEntryWeigher(Weighers.asEntryWeigher(weigher)); return this; } /** - * Specifies an executor for use in catching up the page replacement policy. The - * catch-up phase processes both updates to the retention ordering and writes that - * may trigger an eviction. The delay should be chosen carefully as the map will - * not automatically evict between executions. - *

- * If unspecified or the executor is shutdown, the catching up will be amortized - * on user threads during write operations (or during read operations, in the - * absence of writes). - *

- * A single-threaded {@link ScheduledExecutorService} should be sufficient for - * catching up the page replacement policy in many maps. + * Specifies an algorithm to determine how many the units of capacity an + * entry consumes. The default algorithm bounds the map by the number of + * key-value pairs by giving each entry a weight of 1. * - * @param executor the executor to schedule on - * @param delay the delay between executions - * @param unit the time unit of the delay parameter - * @throws NullPointerException if the executor or time unit is null - * @throws IllegalArgumentException if the delay is less than or equal to zero + * @param weigher the algorithm to determine a entry's weight + * @throws NullPointerException if the weigher is null */ - public Builder catchup( - ScheduledExecutorService executor, - long delay, - TimeUnit unit) { - if (delay <= 0) { - throw new IllegalArgumentException(); - } - checkNotNull(executor); - checkNotNull(unit); - this.executor = executor; - this.delay = delay; - this.unit = unit; + public Builder weigher(EntryWeigher weigher) { + this.weigher = (weigher == Weighers.entrySingleton()) + ? Weighers.entrySingleton() + : new BoundedEntryWeigher(weigher); return this; } /** * Creates a new {@link ConcurrentLinkedHashMap} instance. * - * @throws IllegalStateException if the maximum weighted capacity was not set - * @throws RejectedExecutionException if an executor was specified and the - * catch-up task cannot be scheduled for execution + * @throws IllegalStateException if the maximum weighted capacity was + * not set */ public ConcurrentLinkedHashMap build() { - if (capacity < 0) { - throw new IllegalStateException(); - } - ConcurrentLinkedHashMap map = new ConcurrentLinkedHashMap<>(this); - if (executor != DEFAULT_EXECUTOR) { - ScheduledExecutorService es = (ScheduledExecutorService) executor; - es.scheduleWithFixedDelay(new CatchUpTask(map), delay, delay, unit); - } - return map; + checkState(capacity >= 0); + return new ConcurrentLinkedHashMap(this); } } } From 47a1bb12b79e8d827804fc716c7ca56a71725acf Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 29 Jun 2022 00:28:26 +0100 Subject: [PATCH 22/40] Update ConcurrentLinkedHashMapStressTest.java --- .../util/clhm/ConcurrentLinkedHashMapStressTest.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java index b52b6f7e21..8861ee0a58 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java @@ -13,6 +13,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class ConcurrentLinkedHashMapStressTest { @@ -52,22 +53,17 @@ public void testManyEntries() throws Exception { } executor.awaitTermination(waitSeconds, TimeUnit.SECONDS); - //ConcurrentLinkedHashMap has its own ExecutorService for some background work - //the following code retries the assertions to allow for the evictions to finish final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos(); boolean assertsFailing = true; while(assertsFailing) { try { - assertEquals(maxEntries, clhm.size()); - int matched = 0; + assertTrue("clhm has at least maxEntries", clhm.size() >= maxEntries); for (int i = 0; i < maxKey; i++) { UUID uuid = clhm.get(i); if (uuid != null) { - matched++; assertEquals(map.get(i), clhm.get(i)); } } - assertEquals(maxEntries, matched); assertsFailing = false; } catch (Throwable t) { if (System.nanoTime() > endTime) { From 8463041615f85b84a8efd8bfff97600ac66f42a4 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 29 Jun 2022 00:37:16 +0100 Subject: [PATCH 23/40] Update ConcurrentLinkedHashMapStressTest.java --- .../util/clhm/ConcurrentLinkedHashMapStressTest.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java index 8861ee0a58..d556ff42f5 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java @@ -13,7 +13,6 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; public class ConcurrentLinkedHashMapStressTest { @@ -24,6 +23,12 @@ public class ConcurrentLinkedHashMapStressTest { @Test public void testManyEntries() throws Exception { + for (int i = 0; i < 20; i++) { + _testManyEntries(); + } + } + + public void _testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); @@ -56,8 +61,9 @@ public void testManyEntries() throws Exception { final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos(); boolean assertsFailing = true; while(assertsFailing) { + clhm.drainBuffers(); try { - assertTrue("clhm has at least maxEntries", clhm.size() >= maxEntries); + assertEquals(clhm.size(), maxEntries); for (int i = 0; i < maxKey; i++) { UUID uuid = clhm.get(i); if (uuid != null) { From 64bd51fbf1d712e51d419145ef0ea5d20e14c23c Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 29 Jun 2022 00:41:35 +0100 Subject: [PATCH 24/40] see if updating clhm and map as an atomic operation helps to make test reliable --- .../clhm/ConcurrentLinkedHashMapStressTest.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java index d556ff42f5..747366697a 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java @@ -38,19 +38,23 @@ public void _testManyEntries() throws Exception { final ExecutorService executor = Executors.newFixedThreadPool(threads); try { for (int i = 0; i < maxKey; i++) { - final int key = i; + final Integer key = Integer.valueOf(i); executor.submit(() -> { UUID uuid = UUID.randomUUID(); - clhm.put(key, uuid); - map.put(key, uuid); + synchronized (key) { + clhm.put(key, uuid); + map.put(key, uuid); + } }); } for (int i = 0; i < iterations; i++) { executor.submit(() -> { - int key = rnd.nextInt(maxKey); + Integer key = Integer.valueOf(rnd.nextInt(maxKey)); UUID uuid = UUID.randomUUID(); - clhm.put(key, uuid); - map.put(key, uuid); + synchronized (key) { + clhm.put(key, uuid); + map.put(key, uuid); + } }); } } finally { From f916019c7258241a336cc9d45cf75726b27d984a Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 29 Jun 2022 00:42:05 +0100 Subject: [PATCH 25/40] Update ConcurrentLinkedHashMapStressTest.java --- .../util/clhm/ConcurrentLinkedHashMapStressTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java index 747366697a..aa69f75081 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java @@ -23,12 +23,6 @@ public class ConcurrentLinkedHashMapStressTest { @Test public void testManyEntries() throws Exception { - for (int i = 0; i < 20; i++) { - _testManyEntries(); - } - } - - public void _testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); From 1696ac047c09023cae8e77e1ada0a1dca0bc0e69 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 00:36:33 +0100 Subject: [PATCH 26/40] remove unnecessary concurrency level --- src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java index e48c0a3e32..a010d2b120 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java @@ -34,11 +34,9 @@ public LRUMap(int initialEntries, int maxEntries) { _initialEntries = initialEntries; _maxEntries = maxEntries; - // We'll use concurrency level of 4, seems reasonable _map = new ConcurrentLinkedHashMap.Builder() .initialCapacity(initialEntries) .maximumWeightedCapacity(maxEntries) - .concurrencyLevel(4) .build(); } From 8d29de8f5a19a45b6b30fd64860600d89dfc51fc Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 00:57:00 +0100 Subject: [PATCH 27/40] remove more code for concurrency level --- .../util/clhm/ConcurrentLinkedHashMap.java | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index fb33b7c753..b4c0ca7c35 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -176,7 +176,6 @@ static int ceilingNextPowerOfTwo(int x) { // The backing data store holding the key-value associations final ConcurrentMap> data; - final int concurrencyLevel; // These fields provide support to bound the map by a maximum capacity //@GuardedBy("evictionLock") @@ -212,9 +211,8 @@ static int ceilingNextPowerOfTwo(int x) { @SuppressWarnings({"unchecked", "cast"}) private ConcurrentLinkedHashMap(Builder builder) { // The data store and its maximum capacity - concurrencyLevel = builder.concurrencyLevel; capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); - data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); + data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f); // The eviction support weigher = builder.weigher; @@ -1436,12 +1434,10 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException static final class SerializationProxy implements Serializable { final EntryWeigher weigher; final EvictionListener listener; - final int concurrencyLevel; final Map data; final long capacity; SerializationProxy(ConcurrentLinkedHashMap map) { - concurrencyLevel = map.concurrencyLevel; data = new HashMap(map); capacity = map.capacity.get(); listener = map.listener; @@ -1450,7 +1446,6 @@ static final class SerializationProxy implements Serializable { Object readResolve() { ConcurrentLinkedHashMap map = new Builder() - .concurrencyLevel(concurrencyLevel) .maximumWeightedCapacity(capacity) .listener(listener) .weigher(weigher) @@ -1476,13 +1471,11 @@ Object readResolve() { * } */ public static final class Builder { - static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final int DEFAULT_INITIAL_CAPACITY = 16; EvictionListener listener; EntryWeigher weigher; - int concurrencyLevel; int initialCapacity; long capacity; @@ -1491,7 +1484,6 @@ public Builder() { capacity = -1; weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; - concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; listener = (EvictionListener) DiscardingListener.INSTANCE; } @@ -1524,22 +1516,6 @@ public Builder maximumWeightedCapacity(long capacity) { return this; } - /** - * Specifies the estimated number of concurrently updating threads. The - * implementation performs internal sizing to try to accommodate this many - * threads (default 16). - * - * @param concurrencyLevel the estimated number of concurrently updating - * threads - * @throws IllegalArgumentException if the concurrencyLevel is less than or - * equal to zero - */ - public Builder concurrencyLevel(int concurrencyLevel) { - checkArgument(concurrencyLevel > 0); - this.concurrencyLevel = concurrencyLevel; - return this; - } - /** * Specifies an optional listener that is registered for notification when * an entry is evicted. From a5581f7dc2e78428e4cff40f3c11d77f9d82f7dc Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:05:29 +0100 Subject: [PATCH 28/40] make this CLHM non-serializable --- .../util/clhm/ConcurrentLinkedHashMap.java | 57 +------------------ .../jackson/databind/util/clhm/Weighers.java | 5 +- .../databind/util/clhm/package-info.java | 4 ++ 3 files changed, 7 insertions(+), 59 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index b4c0ca7c35..94ddb65734 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -22,9 +22,6 @@ import static java.util.Collections.unmodifiableMap; import static java.util.Collections.unmodifiableSet; -import java.io.InvalidObjectException; -import java.io.ObjectInputStream; -import java.io.Serializable; import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractQueue; @@ -98,7 +95,7 @@ */ //@ThreadSafe public final class ConcurrentLinkedHashMap extends AbstractMap - implements ConcurrentMap, Serializable { + implements ConcurrentMap { /* * This class performs a best-effort bounding of a ConcurrentHashMap using a @@ -1374,8 +1371,7 @@ Object writeReplace() { } /** A weigher that enforces that the weight falls within a valid range. */ - static final class BoundedEntryWeigher implements EntryWeigher, Serializable { - static final long serialVersionUID = 1; + static final class BoundedEntryWeigher implements EntryWeigher { final EntryWeigher weigher; BoundedEntryWeigher(EntryWeigher weigher) { @@ -1389,10 +1385,6 @@ public int weightOf(K key, V value) { checkArgument(weight >= 1); return weight; } - - Object writeReplace() { - return weigher; - } } /** A queue that discards all additions and is always empty. */ @@ -1412,51 +1404,6 @@ enum DiscardingListener implements EvictionListener { @Override public void onEviction(Object key, Object value) {} } - /* ---------------- Serialization Support -------------- */ - - static final long serialVersionUID = 1; - - Object writeReplace() { - return new SerializationProxy(this); - } - - private void readObject(ObjectInputStream stream) throws InvalidObjectException { - throw new InvalidObjectException("Proxy required"); - } - - /** - * A proxy that is serialized instead of the map. The page-replacement - * algorithm's data structures are not serialized so the deserialized - * instance contains only the entries. This is acceptable as caches hold - * transient data that is recomputable and serialization would tend to be - * used as a fast warm-up process. - */ - static final class SerializationProxy implements Serializable { - final EntryWeigher weigher; - final EvictionListener listener; - final Map data; - final long capacity; - - SerializationProxy(ConcurrentLinkedHashMap map) { - data = new HashMap(map); - capacity = map.capacity.get(); - listener = map.listener; - weigher = map.weigher; - } - - Object readResolve() { - ConcurrentLinkedHashMap map = new Builder() - .maximumWeightedCapacity(capacity) - .listener(listener) - .weigher(weigher) - .build(); - map.putAll(data); - return map; - } - - static final long serialVersionUID = 1; - } - /* ---------------- Builder -------------- */ /** diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java index 5a67ecc209..4f0a4aa2b8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java @@ -17,8 +17,6 @@ import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.checkNotNull; -import java.io.Serializable; - /** * A common set of {@link Weigher} and {@link EntryWeigher} implementations. * @@ -70,8 +68,7 @@ static Weigher singleton() { return (Weigher) SingletonWeigher.INSTANCE; } - static final class EntryWeigherView implements EntryWeigher, Serializable { - static final long serialVersionUID = 1; + static final class EntryWeigherView implements EntryWeigher { final Weigher weigher; EntryWeigherView(Weigher weigher) { diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java index c01e173804..b3f9e807c5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java @@ -18,6 +18,10 @@ * This package contains an implementation of a bounded * {@link java.util.concurrent.ConcurrentMap} data structure. *

+ * This package is intended only for use internally by Jackson libraries and has + * missing features compared to the full + * http://code.google.com/p/concurrentlinkedhashmap/ implementation. + *

* {@link com.fasterxml.jackson.databind.util.clhm.Weigher} is a simple interface * for determining how many units of capacity an entry consumes. Depending on * which concrete Weigher class is used, an entry may consume a different amount From 8e6897d17ca5384b408d9683297863b371773f37 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:11:46 +0100 Subject: [PATCH 29/40] remove EvictionListener --- .../util/clhm/ConcurrentLinkedHashMap.java | 55 +------------------ .../databind/util/clhm/EvictionListener.java | 45 --------------- .../databind/util/clhm/package-info.java | 5 -- 3 files changed, 1 insertion(+), 104 deletions(-) delete mode 100644 src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index 94ddb65734..42aba61639 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -61,22 +61,6 @@ * modifies its weight requires that an update operation is performed on the * map. *

- * An {@link EvictionListener} may be supplied for notification when an entry - * is evicted from the map. This listener is invoked on a caller's thread and - * will not block other threads from operating on the map. An implementation - * should be aware that the caller's thread will not expect long execution - * times or failures as a side effect of the listener being notified. Execution - * safety and a fast turn around time can be achieved by performing the - * operation asynchronously, such as by submitting a task to an - * {@link java.util.concurrent.ExecutorService}. - *

- * The concurrency level determines the number of threads that can - * concurrently modify the table. Using a significantly higher or lower value - * than needed can waste space or lead to thread contention, but an estimate - * within an order of magnitude of the ideal value does not usually have a - * noticeable impact. Because placement in hash tables is essentially random, - * the actual concurrency will vary. - *

* This class and its views and iterators implement all of the * optional methods of the {@link Map} and {@link Iterator} * interfaces. @@ -196,7 +180,6 @@ static int ceilingNextPowerOfTwo(int x) { // These fields provide support for notifying a listener. final Queue> pendingNotifications; - final EvictionListener listener; transient Set keySet; transient Collection values; @@ -233,10 +216,7 @@ private ConcurrentLinkedHashMap(Builder builder) { } // The notification queue and listener - listener = builder.listener; - pendingNotifications = (listener == DiscardingListener.INSTANCE) - ? (Queue>) DISCARDING_QUEUE - : new ConcurrentLinkedQueue>(); + pendingNotifications = (Queue>) DISCARDING_QUEUE; } /** Ensures that the object is not null. */ @@ -288,7 +268,6 @@ public void setCapacity(long capacity) { } finally { evictionLock.unlock(); } - notifyListener(); } /** Determines whether the map has exceeded its capacity. */ @@ -336,7 +315,6 @@ void afterRead(Node node) { final int bufferIndex = readBufferIndex(); final long writeCount = recordRead(bufferIndex, node); drainOnReadIfNeeded(bufferIndex, writeCount); - notifyListener(); } /** Returns the index to the read buffer to record into. */ @@ -393,7 +371,6 @@ void afterWrite(Runnable task) { writeBuffer.add(task); drainStatus.lazySet(REQUIRED); tryToDrainBuffers(); - notifyListener(); } /** @@ -525,14 +502,6 @@ void makeDead(Node node) { } } - /** Notifies the listener of entries that were evicted. */ - void notifyListener() { - Node node; - while ((node = pendingNotifications.poll()) != null) { - listener.onEviction(node.key, node.getValue()); - } - } - /** Adds the node to the page replacement policy. */ final class AddTask implements Runnable { final Node node; @@ -1397,13 +1366,6 @@ static final class DiscardingQueue extends AbstractQueue { @Override public Iterator iterator() { return emptyList().iterator(); } } - /** A listener that ignores all notifications. */ - enum DiscardingListener implements EvictionListener { - INSTANCE; - - @Override public void onEviction(Object key, Object value) {} - } - /* ---------------- Builder -------------- */ /** @@ -1420,7 +1382,6 @@ enum DiscardingListener implements EvictionListener { public static final class Builder { static final int DEFAULT_INITIAL_CAPACITY = 16; - EvictionListener listener; EntryWeigher weigher; int initialCapacity; @@ -1431,7 +1392,6 @@ public Builder() { capacity = -1; weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; - listener = (EvictionListener) DiscardingListener.INSTANCE; } /** @@ -1463,19 +1423,6 @@ public Builder maximumWeightedCapacity(long capacity) { return this; } - /** - * Specifies an optional listener that is registered for notification when - * an entry is evicted. - * - * @param listener the object to forward evicted entries to - * @throws NullPointerException if the listener is null - */ - public Builder listener(EvictionListener listener) { - checkNotNull(listener); - this.listener = listener; - return this; - } - /** * Specifies an algorithm to determine how many the units of capacity a * value consumes. The default algorithm bounds the map by the number of diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java deleted file mode 100644 index 0681ef6732..0000000000 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2010 Google Inc. All Rights Reserved. - * - * 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.fasterxml.jackson.databind.util.clhm; - -/** - * A listener registered for notification when an entry is evicted. An instance - * may be called concurrently by multiple threads to process entries. An - * implementation should avoid performing blocking calls or synchronizing on - * shared resources. - *

- * The listener is invoked by {@link ConcurrentLinkedHashMap} on a caller's - * thread and will not block other threads from operating on the map. An - * implementation should be aware that the caller's thread will not expect - * long execution times or failures as a side effect of the listener being - * notified. Execution safety and a fast turn around time can be achieved by - * performing the operation asynchronously, such as by submitting a task to an - * {@link java.util.concurrent.ExecutorService}. - * - * @author ben.manes@gmail.com (Ben Manes) - * @see - * http://code.google.com/p/concurrentlinkedhashmap/ - */ -interface EvictionListener { - - /** - * A call-back notification that the entry was evicted. - * - * @param key the entry's key - * @param value the entry's value - */ - void onEviction(K key, V value); -} diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java index b3f9e807c5..e209437e31 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java @@ -29,11 +29,6 @@ * {@link com.fasterxml.jackson.databind.util.clhm.Weighers} class provides * utility methods for obtaining the most common kinds of implementations. *

- * {@link com.fasterxml.jackson.databind.util.clhm.EvictionListener} provides the - * ability to be notified when an entry is evicted from the map. An eviction - * occurs when the entry was automatically removed due to the map exceeding a - * capacity threshold. It is not called when an entry was explicitly removed. - *

* The {@link com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap} * class supplies an efficient, scalable, thread-safe, bounded map. As with the * Java Collections Framework the "Concurrent" prefix is used to From 6a5a5b2679889ef7df8d424ad47473591f1f6c11 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:16:44 +0100 Subject: [PATCH 30/40] Revert "remove EvictionListener" This reverts commit 8e6897d17ca5384b408d9683297863b371773f37. --- .../util/clhm/ConcurrentLinkedHashMap.java | 55 ++++++++++++++++++- .../databind/util/clhm/EvictionListener.java | 45 +++++++++++++++ .../databind/util/clhm/package-info.java | 5 ++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index 42aba61639..94ddb65734 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -61,6 +61,22 @@ * modifies its weight requires that an update operation is performed on the * map. *

+ * An {@link EvictionListener} may be supplied for notification when an entry + * is evicted from the map. This listener is invoked on a caller's thread and + * will not block other threads from operating on the map. An implementation + * should be aware that the caller's thread will not expect long execution + * times or failures as a side effect of the listener being notified. Execution + * safety and a fast turn around time can be achieved by performing the + * operation asynchronously, such as by submitting a task to an + * {@link java.util.concurrent.ExecutorService}. + *

+ * The concurrency level determines the number of threads that can + * concurrently modify the table. Using a significantly higher or lower value + * than needed can waste space or lead to thread contention, but an estimate + * within an order of magnitude of the ideal value does not usually have a + * noticeable impact. Because placement in hash tables is essentially random, + * the actual concurrency will vary. + *

* This class and its views and iterators implement all of the * optional methods of the {@link Map} and {@link Iterator} * interfaces. @@ -180,6 +196,7 @@ static int ceilingNextPowerOfTwo(int x) { // These fields provide support for notifying a listener. final Queue> pendingNotifications; + final EvictionListener listener; transient Set keySet; transient Collection values; @@ -216,7 +233,10 @@ private ConcurrentLinkedHashMap(Builder builder) { } // The notification queue and listener - pendingNotifications = (Queue>) DISCARDING_QUEUE; + listener = builder.listener; + pendingNotifications = (listener == DiscardingListener.INSTANCE) + ? (Queue>) DISCARDING_QUEUE + : new ConcurrentLinkedQueue>(); } /** Ensures that the object is not null. */ @@ -268,6 +288,7 @@ public void setCapacity(long capacity) { } finally { evictionLock.unlock(); } + notifyListener(); } /** Determines whether the map has exceeded its capacity. */ @@ -315,6 +336,7 @@ void afterRead(Node node) { final int bufferIndex = readBufferIndex(); final long writeCount = recordRead(bufferIndex, node); drainOnReadIfNeeded(bufferIndex, writeCount); + notifyListener(); } /** Returns the index to the read buffer to record into. */ @@ -371,6 +393,7 @@ void afterWrite(Runnable task) { writeBuffer.add(task); drainStatus.lazySet(REQUIRED); tryToDrainBuffers(); + notifyListener(); } /** @@ -502,6 +525,14 @@ void makeDead(Node node) { } } + /** Notifies the listener of entries that were evicted. */ + void notifyListener() { + Node node; + while ((node = pendingNotifications.poll()) != null) { + listener.onEviction(node.key, node.getValue()); + } + } + /** Adds the node to the page replacement policy. */ final class AddTask implements Runnable { final Node node; @@ -1366,6 +1397,13 @@ static final class DiscardingQueue extends AbstractQueue { @Override public Iterator iterator() { return emptyList().iterator(); } } + /** A listener that ignores all notifications. */ + enum DiscardingListener implements EvictionListener { + INSTANCE; + + @Override public void onEviction(Object key, Object value) {} + } + /* ---------------- Builder -------------- */ /** @@ -1382,6 +1420,7 @@ static final class DiscardingQueue extends AbstractQueue { public static final class Builder { static final int DEFAULT_INITIAL_CAPACITY = 16; + EvictionListener listener; EntryWeigher weigher; int initialCapacity; @@ -1392,6 +1431,7 @@ public Builder() { capacity = -1; weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; + listener = (EvictionListener) DiscardingListener.INSTANCE; } /** @@ -1423,6 +1463,19 @@ public Builder maximumWeightedCapacity(long capacity) { return this; } + /** + * Specifies an optional listener that is registered for notification when + * an entry is evicted. + * + * @param listener the object to forward evicted entries to + * @throws NullPointerException if the listener is null + */ + public Builder listener(EvictionListener listener) { + checkNotNull(listener); + this.listener = listener; + return this; + } + /** * Specifies an algorithm to determine how many the units of capacity a * value consumes. The default algorithm bounds the map by the number of diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java new file mode 100644 index 0000000000..0681ef6732 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 Google Inc. All Rights Reserved. + * + * 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.fasterxml.jackson.databind.util.clhm; + +/** + * A listener registered for notification when an entry is evicted. An instance + * may be called concurrently by multiple threads to process entries. An + * implementation should avoid performing blocking calls or synchronizing on + * shared resources. + *

+ * The listener is invoked by {@link ConcurrentLinkedHashMap} on a caller's + * thread and will not block other threads from operating on the map. An + * implementation should be aware that the caller's thread will not expect + * long execution times or failures as a side effect of the listener being + * notified. Execution safety and a fast turn around time can be achieved by + * performing the operation asynchronously, such as by submitting a task to an + * {@link java.util.concurrent.ExecutorService}. + * + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +interface EvictionListener { + + /** + * A call-back notification that the entry was evicted. + * + * @param key the entry's key + * @param value the entry's value + */ + void onEviction(K key, V value); +} diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java index e209437e31..b3f9e807c5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java @@ -29,6 +29,11 @@ * {@link com.fasterxml.jackson.databind.util.clhm.Weighers} class provides * utility methods for obtaining the most common kinds of implementations. *

+ * {@link com.fasterxml.jackson.databind.util.clhm.EvictionListener} provides the + * ability to be notified when an entry is evicted from the map. An eviction + * occurs when the entry was automatically removed due to the map exceeding a + * capacity threshold. It is not called when an entry was explicitly removed. + *

* The {@link com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap} * class supplies an efficient, scalable, thread-safe, bounded map. As with the * Java Collections Framework the "Concurrent" prefix is used to From 4a26b39ae3b7a3d57ea87820f5e601e4126b9595 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:16:54 +0100 Subject: [PATCH 31/40] Revert "make this CLHM non-serializable" This reverts commit a5581f7dc2e78428e4cff40f3c11d77f9d82f7dc. --- .../util/clhm/ConcurrentLinkedHashMap.java | 57 ++++++++++++++++++- .../jackson/databind/util/clhm/Weighers.java | 5 +- .../databind/util/clhm/package-info.java | 4 -- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index 94ddb65734..b4c0ca7c35 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -22,6 +22,9 @@ import static java.util.Collections.unmodifiableMap; import static java.util.Collections.unmodifiableSet; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractQueue; @@ -95,7 +98,7 @@ */ //@ThreadSafe public final class ConcurrentLinkedHashMap extends AbstractMap - implements ConcurrentMap { + implements ConcurrentMap, Serializable { /* * This class performs a best-effort bounding of a ConcurrentHashMap using a @@ -1371,7 +1374,8 @@ Object writeReplace() { } /** A weigher that enforces that the weight falls within a valid range. */ - static final class BoundedEntryWeigher implements EntryWeigher { + static final class BoundedEntryWeigher implements EntryWeigher, Serializable { + static final long serialVersionUID = 1; final EntryWeigher weigher; BoundedEntryWeigher(EntryWeigher weigher) { @@ -1385,6 +1389,10 @@ public int weightOf(K key, V value) { checkArgument(weight >= 1); return weight; } + + Object writeReplace() { + return weigher; + } } /** A queue that discards all additions and is always empty. */ @@ -1404,6 +1412,51 @@ enum DiscardingListener implements EvictionListener { @Override public void onEviction(Object key, Object value) {} } + /* ---------------- Serialization Support -------------- */ + + static final long serialVersionUID = 1; + + Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Proxy required"); + } + + /** + * A proxy that is serialized instead of the map. The page-replacement + * algorithm's data structures are not serialized so the deserialized + * instance contains only the entries. This is acceptable as caches hold + * transient data that is recomputable and serialization would tend to be + * used as a fast warm-up process. + */ + static final class SerializationProxy implements Serializable { + final EntryWeigher weigher; + final EvictionListener listener; + final Map data; + final long capacity; + + SerializationProxy(ConcurrentLinkedHashMap map) { + data = new HashMap(map); + capacity = map.capacity.get(); + listener = map.listener; + weigher = map.weigher; + } + + Object readResolve() { + ConcurrentLinkedHashMap map = new Builder() + .maximumWeightedCapacity(capacity) + .listener(listener) + .weigher(weigher) + .build(); + map.putAll(data); + return map; + } + + static final long serialVersionUID = 1; + } + /* ---------------- Builder -------------- */ /** diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java index 4f0a4aa2b8..5a67ecc209 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java @@ -17,6 +17,8 @@ import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.checkNotNull; +import java.io.Serializable; + /** * A common set of {@link Weigher} and {@link EntryWeigher} implementations. * @@ -68,7 +70,8 @@ static Weigher singleton() { return (Weigher) SingletonWeigher.INSTANCE; } - static final class EntryWeigherView implements EntryWeigher { + static final class EntryWeigherView implements EntryWeigher, Serializable { + static final long serialVersionUID = 1; final Weigher weigher; EntryWeigherView(Weigher weigher) { diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java index b3f9e807c5..c01e173804 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java @@ -18,10 +18,6 @@ * This package contains an implementation of a bounded * {@link java.util.concurrent.ConcurrentMap} data structure. *

- * This package is intended only for use internally by Jackson libraries and has - * missing features compared to the full - * http://code.google.com/p/concurrentlinkedhashmap/ implementation. - *

* {@link com.fasterxml.jackson.databind.util.clhm.Weigher} is a simple interface * for determining how many units of capacity an entry consumes. Depending on * which concrete Weigher class is used, an entry may consume a different amount From 2e132ca747d3f192e398b08ae67b0d49ae88e48f Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:17:00 +0100 Subject: [PATCH 32/40] Revert "remove more code for concurrency level" This reverts commit 8d29de8f5a19a45b6b30fd64860600d89dfc51fc. --- .../util/clhm/ConcurrentLinkedHashMap.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index b4c0ca7c35..fb33b7c753 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -176,6 +176,7 @@ static int ceilingNextPowerOfTwo(int x) { // The backing data store holding the key-value associations final ConcurrentMap> data; + final int concurrencyLevel; // These fields provide support to bound the map by a maximum capacity //@GuardedBy("evictionLock") @@ -211,8 +212,9 @@ static int ceilingNextPowerOfTwo(int x) { @SuppressWarnings({"unchecked", "cast"}) private ConcurrentLinkedHashMap(Builder builder) { // The data store and its maximum capacity + concurrencyLevel = builder.concurrencyLevel; capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); - data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f); + data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); // The eviction support weigher = builder.weigher; @@ -1434,10 +1436,12 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException static final class SerializationProxy implements Serializable { final EntryWeigher weigher; final EvictionListener listener; + final int concurrencyLevel; final Map data; final long capacity; SerializationProxy(ConcurrentLinkedHashMap map) { + concurrencyLevel = map.concurrencyLevel; data = new HashMap(map); capacity = map.capacity.get(); listener = map.listener; @@ -1446,6 +1450,7 @@ static final class SerializationProxy implements Serializable { Object readResolve() { ConcurrentLinkedHashMap map = new Builder() + .concurrencyLevel(concurrencyLevel) .maximumWeightedCapacity(capacity) .listener(listener) .weigher(weigher) @@ -1471,11 +1476,13 @@ Object readResolve() { * } */ public static final class Builder { + static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final int DEFAULT_INITIAL_CAPACITY = 16; EvictionListener listener; EntryWeigher weigher; + int concurrencyLevel; int initialCapacity; long capacity; @@ -1484,6 +1491,7 @@ public Builder() { capacity = -1; weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; + concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; listener = (EvictionListener) DiscardingListener.INSTANCE; } @@ -1516,6 +1524,22 @@ public Builder maximumWeightedCapacity(long capacity) { return this; } + /** + * Specifies the estimated number of concurrently updating threads. The + * implementation performs internal sizing to try to accommodate this many + * threads (default 16). + * + * @param concurrencyLevel the estimated number of concurrently updating + * threads + * @throws IllegalArgumentException if the concurrencyLevel is less than or + * equal to zero + */ + public Builder concurrencyLevel(int concurrencyLevel) { + checkArgument(concurrencyLevel > 0); + this.concurrencyLevel = concurrencyLevel; + return this; + } + /** * Specifies an optional listener that is registered for notification when * an entry is evicted. From dfcf9768063ed267d4597facc9f43460c51c614f Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:17:11 +0100 Subject: [PATCH 33/40] Revert "remove unnecessary concurrency level" This reverts commit 1696ac047c09023cae8e77e1ada0a1dca0bc0e69. --- src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java index a010d2b120..e48c0a3e32 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java @@ -34,9 +34,11 @@ public LRUMap(int initialEntries, int maxEntries) { _initialEntries = initialEntries; _maxEntries = maxEntries; + // We'll use concurrency level of 4, seems reasonable _map = new ConcurrentLinkedHashMap.Builder() .initialCapacity(initialEntries) .maximumWeightedCapacity(maxEntries) + .concurrencyLevel(4) .build(); } From fc6fa05f0b3557e22e5fae7dad55760132bd0d56 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:19:19 +0100 Subject: [PATCH 34/40] Update package-info.java --- .../fasterxml/jackson/databind/util/clhm/package-info.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java index c01e173804..b3f9e807c5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java @@ -18,6 +18,10 @@ * This package contains an implementation of a bounded * {@link java.util.concurrent.ConcurrentMap} data structure. *

+ * This package is intended only for use internally by Jackson libraries and has + * missing features compared to the full + * http://code.google.com/p/concurrentlinkedhashmap/ implementation. + *

* {@link com.fasterxml.jackson.databind.util.clhm.Weigher} is a simple interface * for determining how many units of capacity an entry consumes. Depending on * which concrete Weigher class is used, an entry may consume a different amount From 6820db2ce5cb1b9940abfdcc6cf8d065e24aea60 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 01:22:33 +0100 Subject: [PATCH 35/40] make some more classes package private --- .../jackson/databind/util/clhm/ConcurrentLinkedHashMap.java | 2 +- .../java/com/fasterxml/jackson/databind/util/clhm/Weighers.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java index fb33b7c753..2b7c4fa606 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java @@ -583,7 +583,7 @@ final class UpdateTask implements Runnable { final int weightDifference; final Node node; - public UpdateTask(Node node, int weightDifference) { + UpdateTask(Node node, int weightDifference) { this.weightDifference = weightDifference; this.node = node; } diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java index 5a67ecc209..a8ebc70584 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java @@ -39,7 +39,7 @@ private Weighers() { * @param weigher the weigher to be "wrapped" in an entry weigher. * @return A entry weigher view of the specified weigher. */ - public static EntryWeigher asEntryWeigher( + static EntryWeigher asEntryWeigher( final Weigher weigher) { return (weigher == singleton()) ? Weighers.entrySingleton() From ff39d5f449ac997548594d1b9b5e7088f12bda42 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 23:38:39 +0100 Subject: [PATCH 36/40] rename internal map --- .../jackson/databind/util/LRUMap.java | 6 +-- .../util/{clhm => internal}/EntryWeigher.java | 2 +- .../{clhm => internal}/EvictionListener.java | 4 +- .../util/{clhm => internal}/LinkedDeque.java | 2 +- .../PrivateMaxEntriesMap.java} | 38 +++++++++---------- .../util/{clhm => internal}/Weigher.java | 2 +- .../util/{clhm => internal}/Weighers.java | 4 +- .../util/{clhm => internal}/package-info.java | 10 ++--- .../{clhm => internal}/CLHMTestlibTests.java | 4 +- .../{clhm => internal}/MapTestFactory.java | 2 +- .../PrivateMaxEntriesMapStressTest.java} | 8 ++-- .../PrivateMaxEntriesMapTest.java} | 8 ++-- 12 files changed, 45 insertions(+), 45 deletions(-) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm => internal}/EntryWeigher.java (95%) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm => internal}/EvictionListener.java (93%) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm => internal}/LinkedDeque.java (99%) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm/ConcurrentLinkedHashMap.java => internal/PrivateMaxEntriesMap.java} (97%) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm => internal}/Weigher.java (95%) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm => internal}/Weighers.java (95%) rename src/main/java/com/fasterxml/jackson/databind/util/{clhm => internal}/package-info.java (82%) rename src/test/java/com/fasterxml/jackson/databind/util/{clhm => internal}/CLHMTestlibTests.java (80%) rename src/test/java/com/fasterxml/jackson/databind/util/{clhm => internal}/MapTestFactory.java (97%) rename src/test/java/com/fasterxml/jackson/databind/util/{clhm/ConcurrentLinkedHashMapStressTest.java => internal/PrivateMaxEntriesMapStressTest.java} (90%) rename src/test/java/com/fasterxml/jackson/databind/util/{clhm/ConcurrentLinkedHashMapTest.java => internal/PrivateMaxEntriesMapTest.java} (88%) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java index e48c0a3e32..d8eab893cc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java @@ -1,6 +1,6 @@ package com.fasterxml.jackson.databind.util; -import com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap; +import com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap; /** * Helper for simple bounded maps used for reusing lookup values. @@ -28,14 +28,14 @@ public class LRUMap protected final int _initialEntries; protected final int _maxEntries; - protected final transient ConcurrentLinkedHashMap _map; + protected final transient PrivateMaxEntriesMap _map; public LRUMap(int initialEntries, int maxEntries) { _initialEntries = initialEntries; _maxEntries = maxEntries; // We'll use concurrency level of 4, seems reasonable - _map = new ConcurrentLinkedHashMap.Builder() + _map = new PrivateMaxEntriesMap.Builder() .initialCapacity(initialEntries) .maximumWeightedCapacity(maxEntries) .concurrencyLevel(4) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EntryWeigher.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java similarity index 95% rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/EntryWeigher.java rename to src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java index 8a812f7199..8def9baed9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EntryWeigher.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; /** * A class that can determine the weight of an entry. The total weight threshold diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/EvictionListener.java similarity index 93% rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java rename to src/main/java/com/fasterxml/jackson/databind/util/internal/EvictionListener.java index 0681ef6732..644f22dc74 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/EvictionListener.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/EvictionListener.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; /** * A listener registered for notification when an entry is evicted. An instance @@ -21,7 +21,7 @@ * implementation should avoid performing blocking calls or synchronizing on * shared resources. *

- * The listener is invoked by {@link ConcurrentLinkedHashMap} on a caller's + * The listener is invoked by {@link PrivateMaxEntriesMap} on a caller's * thread and will not block other threads from operating on the map. An * implementation should be aware that the caller's thread will not expect * long execution times or failures as a side effect of the listener being diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/LinkedDeque.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/LinkedDeque.java similarity index 99% rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/LinkedDeque.java rename to src/main/java/com/fasterxml/jackson/databind/util/internal/LinkedDeque.java index efe89f831e..0ffbd90887 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/LinkedDeque.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/LinkedDeque.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; import java.util.AbstractCollection; import java.util.Collection; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java similarity index 97% rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java rename to src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java index 2b7c4fa606..868c905b58 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; -import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.DrainStatus.IDLE; -import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.DrainStatus.PROCESSING; -import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.DrainStatus.REQUIRED; +import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.DrainStatus.IDLE; +import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.DrainStatus.PROCESSING; +import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.DrainStatus.REQUIRED; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableMap; import static java.util.Collections.unmodifiableSet; @@ -97,7 +97,7 @@ * http://code.google.com/p/concurrentlinkedhashmap/ */ //@ThreadSafe -public final class ConcurrentLinkedHashMap extends AbstractMap +public final class PrivateMaxEntriesMap extends AbstractMap implements ConcurrentMap, Serializable { /* @@ -210,7 +210,7 @@ static int ceilingNextPowerOfTwo(int x) { * Creates an instance based on the builder's configuration. */ @SuppressWarnings({"unchecked", "cast"}) - private ConcurrentLinkedHashMap(Builder builder) { + private PrivateMaxEntriesMap(Builder builder) { // The data store and its maximum capacity concurrencyLevel = builder.concurrencyLevel; capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); @@ -1178,7 +1178,7 @@ V getValue() { /** An adapter to safely externalize the keys. */ final class KeySet extends AbstractSet { - final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + final PrivateMaxEntriesMap map = PrivateMaxEntriesMap.this; @Override public int size() { @@ -1235,7 +1235,7 @@ public K next() { @Override public void remove() { checkState(current != null); - ConcurrentLinkedHashMap.this.remove(current); + PrivateMaxEntriesMap.this.remove(current); current = null; } } @@ -1245,12 +1245,12 @@ final class Values extends AbstractCollection { @Override public int size() { - return ConcurrentLinkedHashMap.this.size(); + return PrivateMaxEntriesMap.this.size(); } @Override public void clear() { - ConcurrentLinkedHashMap.this.clear(); + PrivateMaxEntriesMap.this.clear(); } @Override @@ -1283,14 +1283,14 @@ public V next() { @Override public void remove() { checkState(current != null); - ConcurrentLinkedHashMap.this.remove(current.key); + PrivateMaxEntriesMap.this.remove(current.key); current = null; } } /** An adapter to safely externalize the entries. */ final class EntrySet extends AbstractSet> { - final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + final PrivateMaxEntriesMap map = PrivateMaxEntriesMap.this; @Override public int size() { @@ -1351,7 +1351,7 @@ public Entry next() { @Override public void remove() { checkState(current != null); - ConcurrentLinkedHashMap.this.remove(current.key); + PrivateMaxEntriesMap.this.remove(current.key); current = null; } } @@ -1440,7 +1440,7 @@ static final class SerializationProxy implements Serializable { final Map data; final long capacity; - SerializationProxy(ConcurrentLinkedHashMap map) { + SerializationProxy(PrivateMaxEntriesMap map) { concurrencyLevel = map.concurrencyLevel; data = new HashMap(map); capacity = map.capacity.get(); @@ -1449,7 +1449,7 @@ static final class SerializationProxy implements Serializable { } Object readResolve() { - ConcurrentLinkedHashMap map = new Builder() + PrivateMaxEntriesMap map = new Builder() .concurrencyLevel(concurrencyLevel) .maximumWeightedCapacity(capacity) .listener(listener) @@ -1465,7 +1465,7 @@ Object readResolve() { /* ---------------- Builder -------------- */ /** - * A builder that creates {@link ConcurrentLinkedHashMap} instances. It + * A builder that creates {@link PrivateMaxEntriesMap} instances. It * provides a flexible approach for constructing customized instances with * a named parameter syntax. It can be used in the following manner: *

{@code
@@ -1584,14 +1584,14 @@ public Builder weigher(EntryWeigher weigher) {
         }
 
         /**
-         * Creates a new {@link ConcurrentLinkedHashMap} instance.
+         * Creates a new {@link PrivateMaxEntriesMap} instance.
          *
          * @throws IllegalStateException if the maximum weighted capacity was
          *     not set
          */
-        public ConcurrentLinkedHashMap build() {
+        public PrivateMaxEntriesMap build() {
             checkState(capacity >= 0);
-            return new ConcurrentLinkedHashMap(this);
+            return new PrivateMaxEntriesMap(this);
         }
     }
 }
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weigher.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java
similarity index 95%
rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/Weigher.java
rename to src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java
index b5ea850abd..6ecf324e3f 100644
--- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weigher.java
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.fasterxml.jackson.databind.util.clhm;
+package com.fasterxml.jackson.databind.util.internal;
 
 /**
  * A class that can determine the weight of a value. The total weight threshold
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java
similarity index 95%
rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java
rename to src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java
index a8ebc70584..879e533962 100644
--- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/Weighers.java
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.fasterxml.jackson.databind.util.clhm;
+package com.fasterxml.jackson.databind.util.internal;
 
-import static com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap.checkNotNull;
+import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.checkNotNull;
 
 import java.io.Serializable;
 
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/package-info.java
similarity index 82%
rename from src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java
rename to src/main/java/com/fasterxml/jackson/databind/util/internal/package-info.java
index b3f9e807c5..f7de34a700 100644
--- a/src/main/java/com/fasterxml/jackson/databind/util/clhm/package-info.java
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/package-info.java
@@ -22,19 +22,19 @@
  * missing features compared to the full 
  * http://code.google.com/p/concurrentlinkedhashmap/ implementation.
  * 

- * {@link com.fasterxml.jackson.databind.util.clhm.Weigher} is a simple interface + * {@link com.fasterxml.jackson.databind.util.internal.Weigher} is a simple interface * for determining how many units of capacity an entry consumes. Depending on * which concrete Weigher class is used, an entry may consume a different amount * of space within the cache. The - * {@link com.fasterxml.jackson.databind.util.clhm.Weighers} class provides + * {@link com.fasterxml.jackson.databind.util.internal.Weighers} class provides * utility methods for obtaining the most common kinds of implementations. *

- * {@link com.fasterxml.jackson.databind.util.clhm.EvictionListener} provides the + * {@link com.fasterxml.jackson.databind.util.internal.EvictionListener} provides the * ability to be notified when an entry is evicted from the map. An eviction * occurs when the entry was automatically removed due to the map exceeding a * capacity threshold. It is not called when an entry was explicitly removed. *

- * The {@link com.fasterxml.jackson.databind.util.clhm.ConcurrentLinkedHashMap} + * The {@link com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap} * class supplies an efficient, scalable, thread-safe, bounded map. As with the * Java Collections Framework the "Concurrent" prefix is used to * indicate that the map is not governed by a single exclusion lock. @@ -42,4 +42,4 @@ * @see * http://code.google.com/p/concurrentlinkedhashmap/ */ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/CLHMTestlibTests.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java similarity index 80% rename from src/test/java/com/fasterxml/jackson/databind/util/clhm/CLHMTestlibTests.java rename to src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java index b8496a2e3c..a599781d2b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/CLHMTestlibTests.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; import junit.framework.Test; import junit.framework.TestCase; @@ -14,7 +14,7 @@ public static Test suite() { private static void addCLHMViewTests(TestSuite suite) { suite.addTest(MapTestFactory.suite("CLHMView", MapTestFactory.synchronousGenerator( - () -> new ConcurrentLinkedHashMap.Builder() + () -> new PrivateMaxEntriesMap.Builder() .maximumWeightedCapacity(10).build())) ); } diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/MapTestFactory.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/MapTestFactory.java similarity index 97% rename from src/test/java/com/fasterxml/jackson/databind/util/clhm/MapTestFactory.java rename to src/test/java/com/fasterxml/jackson/databind/util/internal/MapTestFactory.java index 4303155b78..d0b23f8bcf 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/MapTestFactory.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/MapTestFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; import java.util.Map; import java.util.function.Supplier; diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java similarity index 90% rename from src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java rename to src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java index aa69f75081..c49a7a3a6a 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; import org.junit.Test; @@ -14,7 +14,7 @@ import static org.junit.Assert.assertEquals; -public class ConcurrentLinkedHashMapStressTest { +public class PrivateMaxEntriesMapStressTest { //increase these to increase the stress private static int iterations = 100000; @@ -26,8 +26,8 @@ public void testManyEntries() throws Exception { final int maxEntries = 30; final int maxKey = 100; final Random rnd = new Random(); - final ConcurrentLinkedHashMap clhm = - new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(maxEntries).build(); + final PrivateMaxEntriesMap clhm = + new PrivateMaxEntriesMap.Builder().maximumWeightedCapacity(maxEntries).build(); final Map map = new ConcurrentHashMap<>(); final ExecutorService executor = Executors.newFixedThreadPool(threads); try { diff --git a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapTest.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java similarity index 88% rename from src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapTest.java rename to src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java index a4af3e772d..2ce15c4a0c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/clhm/ConcurrentLinkedHashMapTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. ****************************************************************/ -package com.fasterxml.jackson.databind.util.clhm; +package com.fasterxml.jackson.databind.util.internal; import org.junit.Test; @@ -24,11 +24,11 @@ import static org.junit.Assert.assertNull; //copied from https://github.com/apache/cayenne/blob/b156addac1c8e4079fa88e977fee609210c5da69/cayenne-server/src/test/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapTest.java -public class ConcurrentLinkedHashMapTest { +public class PrivateMaxEntriesMapTest { @Test public void testPutGet() { - ConcurrentLinkedHashMap m = new ConcurrentLinkedHashMap.Builder() + PrivateMaxEntriesMap m = new PrivateMaxEntriesMap.Builder() .maximumWeightedCapacity(10).build(); assertEquals(0, m.size()); @@ -44,7 +44,7 @@ public void testPutGet() { @Test public void testLRU() { - ConcurrentLinkedHashMap m = new ConcurrentLinkedHashMap.Builder() + PrivateMaxEntriesMap m = new PrivateMaxEntriesMap.Builder() .maximumWeightedCapacity(5).build(); assertEquals(0, m.size()); From b4380915b92e2e3cf3634565832a1f914f507976 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 30 Jun 2022 23:55:34 +0100 Subject: [PATCH 37/40] remove more weigher logic --- .../jackson/databind/util/LRUMap.java | 2 +- .../util/internal/PrivateMaxEntriesMap.java | 83 +++---------------- .../databind/util/internal/Weighers.java | 50 ----------- .../util/internal/CLHMTestlibTests.java | 2 +- .../PrivateMaxEntriesMapStressTest.java | 2 +- .../internal/PrivateMaxEntriesMapTest.java | 4 +- 6 files changed, 18 insertions(+), 125 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java index d8eab893cc..88aa4eac59 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java @@ -37,7 +37,7 @@ public LRUMap(int initialEntries, int maxEntries) // We'll use concurrency level of 4, seems reasonable _map = new PrivateMaxEntriesMap.Builder() .initialCapacity(initialEntries) - .maximumWeightedCapacity(maxEntries) + .maximumCapacity(maxEntries) .concurrencyLevel(4) .build(); } diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java index 868c905b58..f4b3b00fa1 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java @@ -55,7 +55,7 @@ * through a {@link Builder}. *

* An entry is evicted from the map when the weighted capacity exceeds - * its maximum weighted capacity threshold. A {@link EntryWeigher} + * its maximum capacity threshold. A {@link EntryWeigher} * determines how many units of capacity that an entry consumes. The default * weigher assigns each value a weight of 1 to bound the map by the * total number of key-value pairs. A map that holds collections may choose to @@ -142,7 +142,7 @@ public final class PrivateMaxEntriesMap extends AbstractMap /** The number of CPUs */ static final int NCPU = Runtime.getRuntime().availableProcessors(); - /** The maximum weighted capacity of the map. */ + /** The maximum capacity of the map. */ static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE; /** The number of read buffers to use. */ @@ -268,19 +268,19 @@ static void checkState(boolean expression) { /* ---------------- Eviction Support -------------- */ /** - * Retrieves the maximum weighted capacity of the map. + * Retrieves the maximum capacity of the map. * - * @return the maximum weighted capacity + * @return the maximum capacity */ public long capacity() { return capacity.get(); } /** - * Sets the maximum weighted capacity of the map and eagerly evicts entries + * Sets the maximum capacity of the map and eagerly evicts entries * until it shrinks to the appropriate size. * - * @param capacity the maximum weighted capacity of the map + * @param capacity the maximum capacity of the map * @throws IllegalArgumentException if the capacity is negative */ public void setCapacity(long capacity) { @@ -1375,28 +1375,6 @@ Object writeReplace() { } } - /** A weigher that enforces that the weight falls within a valid range. */ - static final class BoundedEntryWeigher implements EntryWeigher, Serializable { - static final long serialVersionUID = 1; - final EntryWeigher weigher; - - BoundedEntryWeigher(EntryWeigher weigher) { - checkNotNull(weigher); - this.weigher = weigher; - } - - @Override - public int weightOf(K key, V value) { - int weight = weigher.weightOf(key, value); - checkArgument(weight >= 1); - return weight; - } - - Object writeReplace() { - return weigher; - } - } - /** A queue that discards all additions and is always empty. */ static final class DiscardingQueue extends AbstractQueue { @Override public boolean add(Object e) { return true; } @@ -1450,10 +1428,7 @@ static final class SerializationProxy implements Serializable { Object readResolve() { PrivateMaxEntriesMap map = new Builder() - .concurrencyLevel(concurrencyLevel) - .maximumWeightedCapacity(capacity) - .listener(listener) - .weigher(weigher) + .maximumCapacity(capacity) .build(); map.putAll(data); return map; @@ -1470,8 +1445,7 @@ Object readResolve() { * a named parameter syntax. It can be used in the following manner: *
{@code
      * ConcurrentMap> graph = new Builder>()
-     *     .maximumWeightedCapacity(5000)
-     *     .weigher(Weighers.set())
+     *     .maximumCapacity(5000)
      *     .build();
      * }
*/ @@ -1511,14 +1485,13 @@ public Builder initialCapacity(int initialCapacity) { } /** - * Specifies the maximum weighted capacity to coerce the map to and may + * Specifies the maximum capacity to coerce the map to and may * exceed it temporarily. * - * @param capacity the weighted threshold to bound the map by - * @throws IllegalArgumentException if the maximumWeightedCapacity is - * negative + * @param capacity the threshold to bound the map by + * @throws IllegalArgumentException if the maximumCapacity is negative */ - public Builder maximumWeightedCapacity(long capacity) { + public Builder maximumCapacity(long capacity) { checkArgument(capacity >= 0); this.capacity = capacity; return this; @@ -1553,40 +1526,10 @@ public Builder listener(EvictionListener listener) { return this; } - /** - * Specifies an algorithm to determine how many the units of capacity a - * value consumes. The default algorithm bounds the map by the number of - * key-value pairs by giving each entry a weight of 1. - * - * @param weigher the algorithm to determine a value's weight - * @throws NullPointerException if the weigher is null - */ - public Builder weigher(Weigher weigher) { - this.weigher = (weigher == Weighers.singleton()) - ? Weighers.entrySingleton() - : new BoundedEntryWeigher(Weighers.asEntryWeigher(weigher)); - return this; - } - - /** - * Specifies an algorithm to determine how many the units of capacity an - * entry consumes. The default algorithm bounds the map by the number of - * key-value pairs by giving each entry a weight of 1. - * - * @param weigher the algorithm to determine a entry's weight - * @throws NullPointerException if the weigher is null - */ - public Builder weigher(EntryWeigher weigher) { - this.weigher = (weigher == Weighers.entrySingleton()) - ? Weighers.entrySingleton() - : new BoundedEntryWeigher(weigher); - return this; - } - /** * Creates a new {@link PrivateMaxEntriesMap} instance. * - * @throws IllegalStateException if the maximum weighted capacity was + * @throws IllegalStateException if the maximum capacity was * not set */ public PrivateMaxEntriesMap build() { diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java index 879e533962..39d906801f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java @@ -32,20 +32,6 @@ private Weighers() { throw new AssertionError(); } - /** - * A entry weigher backed by the specified weigher. The weight of the value - * determines the weight of the entry. - * - * @param weigher the weigher to be "wrapped" in an entry weigher. - * @return A entry weigher view of the specified weigher. - */ - static EntryWeigher asEntryWeigher( - final Weigher weigher) { - return (weigher == singleton()) - ? Weighers.entrySingleton() - : new EntryWeigherView(weigher); - } - /** * A weigher where an entry has a weight of 1. A map bounded with * this weigher will evict when the number of key-value pairs exceeds the @@ -58,33 +44,6 @@ static EntryWeigher entrySingleton() { return (EntryWeigher) SingletonEntryWeigher.INSTANCE; } - /** - * A weigher where a value has a weight of 1. A map bounded with - * this weigher will evict when the number of key-value pairs exceeds the - * capacity. - * - * @return A weigher where a value takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - static Weigher singleton() { - return (Weigher) SingletonWeigher.INSTANCE; - } - - static final class EntryWeigherView implements EntryWeigher, Serializable { - static final long serialVersionUID = 1; - final Weigher weigher; - - EntryWeigherView(Weigher weigher) { - checkNotNull(weigher); - this.weigher = weigher; - } - - @Override - public int weightOf(K key, V value) { - return weigher.weightOf(value); - } - } - enum SingletonEntryWeigher implements EntryWeigher { INSTANCE; @@ -93,13 +52,4 @@ public int weightOf(Object key, Object value) { return 1; } } - - enum SingletonWeigher implements Weigher { - INSTANCE; - - @Override - public int weightOf(Object value) { - return 1; - } - } } diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java index a599781d2b..6349aa3186 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java @@ -15,7 +15,7 @@ public static Test suite() { private static void addCLHMViewTests(TestSuite suite) { suite.addTest(MapTestFactory.suite("CLHMView", MapTestFactory.synchronousGenerator( () -> new PrivateMaxEntriesMap.Builder() - .maximumWeightedCapacity(10).build())) + .maximumCapacity(10).build())) ); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java index c49a7a3a6a..1d0d0cbcde 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java @@ -27,7 +27,7 @@ public void testManyEntries() throws Exception { final int maxKey = 100; final Random rnd = new Random(); final PrivateMaxEntriesMap clhm = - new PrivateMaxEntriesMap.Builder().maximumWeightedCapacity(maxEntries).build(); + new PrivateMaxEntriesMap.Builder().maximumCapacity(maxEntries).build(); final Map map = new ConcurrentHashMap<>(); final ExecutorService executor = Executors.newFixedThreadPool(threads); try { diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java index 2ce15c4a0c..eccb9e86d3 100644 --- a/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java @@ -29,7 +29,7 @@ public class PrivateMaxEntriesMapTest { @Test public void testPutGet() { PrivateMaxEntriesMap m = new PrivateMaxEntriesMap.Builder() - .maximumWeightedCapacity(10).build(); + .maximumCapacity(10).build(); assertEquals(0, m.size()); m.put("k1", 100); @@ -45,7 +45,7 @@ public void testPutGet() { @Test public void testLRU() { PrivateMaxEntriesMap m = new PrivateMaxEntriesMap.Builder() - .maximumWeightedCapacity(5).build(); + .maximumCapacity(5).build(); assertEquals(0, m.size()); m.put("k1", 100); From f85c808f96e6abc557636705645d6c650d75f02a Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 1 Jul 2022 00:00:51 +0100 Subject: [PATCH 38/40] Update LRUMap.java --- .../com/fasterxml/jackson/databind/util/LRUMap.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java index 88aa4eac59..2ce8fe3a09 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java @@ -9,16 +9,11 @@ * on assumption that all use cases are for caching where persistence * does not make sense. The only thing serialized is the cache size of Map. *

- * NOTE: since version 2.4.2, this is NOT an LRU-based at all; reason - * being that it is not possible to use JDK components that do LRU _AND_ perform - * well wrt synchronization on multi-core systems. So we choose efficient synchronization - * over potentially more efficient handling of entries. + * NOTE: since Jackson 2.14, the implementation evicts the least recently used + * entry when max size is reached. *

- * And yes, there are efficient LRU implementations such as - * concurrentlinkedhashmap; - * but at this point we really try to keep external deps to minimum. - * Plan from Jackson 2.12 is to focus more on pluggability as {@link LookupCache} and - * let users, frameworks, provide their own cache implementations. + * Since Jackson 2.12, there has been pluggable {@link LookupCache} interface which + * allows users, frameworks, provide their own cache implementations. */ public class LRUMap implements LookupCache, // since 2.12 From 3bc8acd368e20ba8717825ee2b3b0c978fb5709c Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 1 Jul 2022 01:02:29 +0100 Subject: [PATCH 39/40] remove Weigher interface --- .../databind/util/internal/Weigher.java | 36 ------------------- .../databind/util/internal/Weighers.java | 2 +- 2 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java deleted file mode 100644 index 6ecf324e3f..0000000000 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weigher.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2010 Google Inc. All Rights Reserved. - * - * 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.fasterxml.jackson.databind.util.internal; - -/** - * A class that can determine the weight of a value. The total weight threshold - * is used to determine when an eviction is required. - * - * @author ben.manes@gmail.com (Ben Manes) - * @see - * http://code.google.com/p/concurrentlinkedhashmap/ - */ -interface Weigher { - - /** - * Measures an object's weight to determine how many units of capacity that - * the value consumes. A value must consume a minimum of one unit. - * - * @param value the object to weigh - * @return the object's weight - */ - int weightOf(V value); -} diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java index 39d906801f..caf268c9d5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java @@ -20,7 +20,7 @@ import java.io.Serializable; /** - * A common set of {@link Weigher} and {@link EntryWeigher} implementations. + * A common set of {@link EntryWeigher} implementations. * * @author ben.manes@gmail.com (Ben Manes) * @see From a0e8839e8826ff016aae5d3e6c975c3799ce5227 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 1 Jul 2022 01:11:53 +0100 Subject: [PATCH 40/40] remove EntryWeigher interface --- .../databind/util/internal/EntryWeigher.java | 37 ------------- .../util/internal/PrivateMaxEntriesMap.java | 31 +++-------- .../databind/util/internal/Weighers.java | 55 ------------------- 3 files changed, 7 insertions(+), 116 deletions(-) delete mode 100644 src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java delete mode 100644 src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java deleted file mode 100644 index 8def9baed9..0000000000 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/EntryWeigher.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012 Google Inc. All Rights Reserved. - * - * 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.fasterxml.jackson.databind.util.internal; - -/** - * A class that can determine the weight of an entry. The total weight threshold - * is used to determine when an eviction is required. - * - * @author ben.manes@gmail.com (Ben Manes) - * @see - * http://code.google.com/p/concurrentlinkedhashmap/ - */ -interface EntryWeigher { - - /** - * Measures an entry's weight to determine how many units of capacity that - * the key and value consumes. An entry must consume a minimum of one unit. - * - * @param key the key to weigh - * @param value the value to weigh - * @return the entry's weight - */ - int weightOf(K key, V value); -} diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java index f4b3b00fa1..9058171dce 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java @@ -54,15 +54,8 @@ * map does not have a publicly visible constructor and instances are created * through a {@link Builder}. *

- * An entry is evicted from the map when the weighted capacity exceeds - * its maximum capacity threshold. A {@link EntryWeigher} - * determines how many units of capacity that an entry consumes. The default - * weigher assigns each value a weight of 1 to bound the map by the - * total number of key-value pairs. A map that holds collections may choose to - * weigh values by the number of elements in the collection and bound the map - * by the total number of elements that it contains. A change to a value that - * modifies its weight requires that an update operation is performed on the - * map. + * An entry is evicted from the map when the entry size exceeds + * its maximum capacity threshold. *

* An {@link EvictionListener} may be supplied for notification when an entry * is evicted from the map. This listener is invoked on a caller's thread and @@ -196,7 +189,6 @@ static int ceilingNextPowerOfTwo(int x) { final AtomicReference>[][] readBuffers; final AtomicReference drainStatus; - final EntryWeigher weigher; // These fields provide support for notifying a listener. final Queue> pendingNotifications; @@ -217,7 +209,6 @@ private PrivateMaxEntriesMap(Builder builder) { data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); // The eviction support - weigher = builder.weigher; evictionLock = new ReentrantLock(); weightedSize = new AtomicLong(); evictionDeque = new LinkedDeque>(); @@ -713,7 +704,7 @@ V put(K key, V value, boolean onlyIfAbsent) { checkNotNull(key); checkNotNull(value); - final int weight = weigher.weightOf(key, value); + final int weight = 1; final WeightedValue weightedValue = new WeightedValue(value, weight); final Node node = new Node(key, weightedValue); @@ -790,7 +781,7 @@ public V replace(K key, V value) { checkNotNull(key); checkNotNull(value); - final int weight = weigher.weightOf(key, value); + final int weight = 1; final WeightedValue weightedValue = new WeightedValue(value, weight); final Node node = data.get(key); @@ -820,7 +811,7 @@ public boolean replace(K key, V oldValue, V newValue) { checkNotNull(oldValue); checkNotNull(newValue); - final int weight = weigher.weightOf(key, newValue); + final int weight = 1; final WeightedValue newWeightedValue = new WeightedValue(newValue, weight); final Node node = data.get(key); @@ -928,9 +919,7 @@ Set orderedKeySet(boolean ascending, int limit) { try { drainBuffers(); - final int initialCapacity = (weigher == Weighers.entrySingleton()) - ? Math.min(limit, (int) weightedSize()) - : 16; + final int initialCapacity = Math.min(limit, (int) weightedSize()); final Set keys = new LinkedHashSet(initialCapacity); final Iterator> iterator = ascending ? evictionDeque.iterator() @@ -1038,9 +1027,7 @@ Map orderedMap(boolean ascending, int limit) { try { drainBuffers(); - final int initialCapacity = (weigher == Weighers.entrySingleton()) - ? Math.min(limit, (int) weightedSize()) - : 16; + final int initialCapacity = Math.min(limit, (int) weightedSize()); final Map map = new LinkedHashMap(initialCapacity); final Iterator> iterator = ascending ? evictionDeque.iterator() @@ -1412,7 +1399,6 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException * used as a fast warm-up process. */ static final class SerializationProxy implements Serializable { - final EntryWeigher weigher; final EvictionListener listener; final int concurrencyLevel; final Map data; @@ -1423,7 +1409,6 @@ static final class SerializationProxy implements Serializable { data = new HashMap(map); capacity = map.capacity.get(); listener = map.listener; - weigher = map.weigher; } Object readResolve() { @@ -1454,7 +1439,6 @@ public static final class Builder { static final int DEFAULT_INITIAL_CAPACITY = 16; EvictionListener listener; - EntryWeigher weigher; int concurrencyLevel; int initialCapacity; @@ -1463,7 +1447,6 @@ public static final class Builder { @SuppressWarnings("unchecked") public Builder() { capacity = -1; - weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; listener = (EvictionListener) DiscardingListener.INSTANCE; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java deleted file mode 100644 index caf268c9d5..0000000000 --- a/src/main/java/com/fasterxml/jackson/databind/util/internal/Weighers.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2010 Google Inc. All Rights Reserved. - * - * 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.fasterxml.jackson.databind.util.internal; - -import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.checkNotNull; - -import java.io.Serializable; - -/** - * A common set of {@link EntryWeigher} implementations. - * - * @author ben.manes@gmail.com (Ben Manes) - * @see - * http://code.google.com/p/concurrentlinkedhashmap/ - */ -final class Weighers { - - private Weighers() { - throw new AssertionError(); - } - - /** - * A weigher where an entry has a weight of 1. A map bounded with - * this weigher will evict when the number of key-value pairs exceeds the - * capacity. - * - * @return A weigher where a value takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - static EntryWeigher entrySingleton() { - return (EntryWeigher) SingletonEntryWeigher.INSTANCE; - } - - enum SingletonEntryWeigher implements EntryWeigher { - INSTANCE; - - @Override - public int weightOf(Object key, Object value) { - return 1; - } - } -}