Skip to content

Commit 4b3fd3b

Browse files
committed
re
1 parent eb08da7 commit 4b3fd3b

File tree

5 files changed

+140
-180
lines changed

5 files changed

+140
-180
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ target/
22
!.mvn/wrapper/maven-wrapper.jar
33
!**/src/main/**/target/
44
!**/src/test/**/target/
5+
.flattened-pom.xml
56

67
### IntelliJ IDEA ###
78
.idea/

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ import net.lbruun.cache.MemorySensitiveCache;
7676
@Configuration
7777
public class MyConfig {
7878

79-
// Spring will automatically call the close() method on this bean
80-
// when the application context is closed.
8179
@Bean
8280
public MemorySensitiveCacheCache <Integer, String> cacheOfAllGoodThings() {
8381
// Create a cache which retains hard references to the 100 most recently used entries
@@ -86,4 +84,3 @@ public class MyConfig {
8684
}
8785
```
8886

89-
You should close the cache when it is no longer needed by calling the `close()` method.

pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ limitations under the License.
142142
</plugin>
143143

144144

145-
<!-- The 'flatten-maven-plugin' is required because we use Maven CI Friendly feature
146-
and because this is a multi-module project.
145+
<!-- The 'flatten-maven-plugin' is not strictly required because this is a
146+
single-module project. However, it still makes sense to slim down the
147+
consumer POM.
147148
See https://maven.apache.org/maven-ci-friendly.html for more information.
148149
In Maven 4 we can finally get rid of this plugin. -->
149150
<plugin>

src/main/java/net/lbruun/cache/MemorySensitiveCache.java

Lines changed: 48 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,23 @@
4242
* countermeasure to the non-predictability of the cache's eviction as explained above: at least for
4343
* the X most recently used elements, the retention is guaranteed.
4444
*
45+
* <p>Entries that have been garbage collected remain in the cache using the memory consumption of a
46+
* key and an empty reference. Not a lot, but the cache would grow indefinitely if such empty
47+
* entries were not removed. Therefore, such entries are removed from the cache on every "touch" the
48+
* cache, i.e. read, write or "size" operations. This is a very, very fast operation and can hardly
49+
* show in measurements. However, garbage collection often occurs in "bursts", meaning that it may
50+
* happen there is suddenly a large bunch of entries that need to be expunged. Theoretically, this
51+
* can block the current cache operation for a short period of time, say 2ms. In practice, this has
52+
* not been observed to be a problem. Even so, the behavior can be tuned via the {@code
53+
* maxExpungePerReadOperation} parameter in the constructor. The default value of 100 means that at
54+
* most GC'ed 100 entries will be expunged per read operation. If there are more entries to expunge,
55+
* they will be expunged on the next read operation. By contrast, write operations and "size"
56+
* operations will always expunge all GC'ed entries in one go.
57+
*
4558
* <p>The cache is fully thread-safe.
4659
*
4760
* <p>The cache only allows non-null values.
4861
*
49-
* <p>The cache must be {@link #close() closed} when it is no longer needed. If not, the background
50-
* thread will continue to run.
51-
*
5262
* @implNote At any point in time, the map which is the "backend" for this cache, will contain both
5363
* real usable values and values which has been garbage collected and are therefore empty. The
5464
* latter, empty values, are effectively values that are "marked for deletion" from the map.
@@ -57,23 +67,17 @@
5767
* @param <K> key type
5868
* @param <V> value type
5969
*/
60-
public class MemorySensitiveCache<K, V> implements AutoCloseable {
70+
public class MemorySensitiveCache<K, V> {
6171

6272
public static final int USE_DEFAULT_MAP_INITIAL_CAPACITY = -1;
6373
private final ConcurrentMap<K, SoftValue<K, V>> map;
6474
private final ReferenceQueue<V> referenceQueue = new ReferenceQueue<>();
6575
private final SimpleQueue<K, V> hardCache;
6676
private final Class<K> keyClass;
67-
private volatile boolean closed = false;
6877

6978
/**
7079
* Creates a cache.
7180
*
72-
* <p>The cache has a background reaper thread which is responsible for removing garbage collected
73-
* values from the cache's underlying map. Such values take up very little space in the map;
74-
* nevertheless, they need to be removed to stop the map from growing indefinitely. Because they
75-
* take up so little space, the reaper does not have to fire that often.
76-
*
7781
* @param keyClass class for the key
7882
* @param hardRefSize number of elements which are "hard referenced" meaning they are never GC'ed.
7983
* Value must be &ge; 0. Setting this value too high may result in an eventual {@link
@@ -86,10 +90,17 @@ public class MemorySensitiveCache<K, V> implements AutoCloseable {
8690
* @param initialCapacity the initial capacity of the map used by this cache. If {@link
8791
* #USE_DEFAULT_MAP_INITIAL_CAPACITY} then the JDK's default value will be used, typically 16.
8892
* See {@link ConcurrentHashMap#ConcurrentHashMap(int)} for more information.
89-
* @param reaperInitialDelay the initial delay before the reaper fires.
90-
* @param reaperInterval the periodic interval at which the reaper fires.
93+
* @param maxExpungePerReadOperation Maximum number of GC'ed entries to expunge from the cache on
94+
* a single "touch" of the cache. This applies to read-operations only (i.e. get, contains,
95+
* etc). Write operations and "size" operations will always expunge all GC'ed entries in one
96+
* go. The value must be &ge; 0. Set to {@link Integer#MAX_VALUE} to effectively disable the
97+
* feature. Default value is 100.
9198
*/
92-
public MemorySensitiveCache(final Class<K> keyClass, int hardRefSize, int initialCapacity) {
99+
public MemorySensitiveCache(
100+
final Class<K> keyClass,
101+
int hardRefSize,
102+
int initialCapacity,
103+
int maxExpungePerReadOperation) {
93104
Objects.requireNonNull(keyClass, "keyClass must not be null");
94105

95106
this.keyClass = keyClass;
@@ -107,20 +118,10 @@ public MemorySensitiveCache(final Class<K> keyClass, int hardRefSize, int initia
107118
* @param keyClass class for the key
108119
* @param hardRefSize number of elements which are "hard referenced" meaning they are never GC'ed.
109120
* (value must be &ge; 0)
110-
* @see #MemorySensitiveCache(Class, int, int)
121+
* @see #MemorySensitiveCache(Class, int, int, int)
111122
*/
112123
public MemorySensitiveCache(final Class<K> keyClass, int hardRefSize) {
113-
this(keyClass, hardRefSize, USE_DEFAULT_MAP_INITIAL_CAPACITY);
114-
}
115-
116-
private static ScheduledExecutorService defaultScheduledExecutorService() {
117-
return Executors.newScheduledThreadPool(
118-
1, // pool size
119-
r -> {
120-
Thread t = new Thread(r);
121-
t.setDaemon(true);
122-
return t;
123-
});
124+
this(keyClass, hardRefSize, USE_DEFAULT_MAP_INITIAL_CAPACITY, 100);
124125
}
125126

126127
/**
@@ -130,13 +131,20 @@ private static ScheduledExecutorService defaultScheduledExecutorService() {
130131
* <p>The method is invoked for every access to the cache. According to the JDK's Javadoc (see
131132
* https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/ref/package-summary.html#notification-heading)
132133
* this is very fast and should not be a performance issue. It is also how WeakHashMap does it.
134+
* This has also been verified by oww measurements.
133135
*/
134-
private void expungeStaleEntries() {
136+
private void expungeStaleEntries(boolean allowBlocking) {
135137
// Remove objects which have been GC'ed
138+
// long startTime = System.nanoTime();
136139
int count = 0;
137140

138-
// Avoid handling bursts of garbage collected objects in one go.
139-
while (count < 100 && (!closed)) {
141+
// Avoid handling bursts of garbage collected objects in one go for read operations
142+
// (allowBlocking = false)
143+
// in contrast to write operations (allowBlocking = true) where we want to get rid of all stale
144+
// entries as quickly as possible.
145+
// It is debatable if the split should be 100 or some other number. It is also debatable if
146+
// there should be a split at all since indeed it seems to be a very fast operation.
147+
while ((allowBlocking) || (count < 100)) {
140148
Reference<? extends V> ref = referenceQueue.poll();
141149
if (ref == null) {
142150
break;
@@ -153,12 +161,15 @@ private void expungeStaleEntries() {
153161
K key = keyClass.cast(oKey);
154162
// The value may theoretically have entered into the cache (again)
155163
// in the meantime. So remove _conditionally_.
156-
if (!closed) {
157-
removeIfEmpty(key, false);
158-
}
164+
removeIfEmpty(key, false);
159165
}
160166
}
161167
}
168+
// if (count > 0) {
169+
// long endTime = System.nanoTime();
170+
// System.out.println("Took " + (endTime - startTime)/1000000 + " ms to expunge " + count +
171+
// " entries");
172+
// }
162173
}
163174

164175
/**
@@ -169,10 +180,7 @@ private void expungeStaleEntries() {
169180
*/
170181
public V get(K key) {
171182
Objects.requireNonNull(key, "key cannot be null");
172-
if (closed) {
173-
throw new IllegalStateException("Cache has been closed");
174-
}
175-
expungeStaleEntries();
183+
expungeStaleEntries(false);
176184
SoftValue<K, V> softValue =
177185
map.compute(
178186
key,
@@ -249,10 +257,7 @@ private SoftValue<K, V> compute00(
249257
}
250258

251259
private void compute0(K key, Function<K, V> mappingFunction) {
252-
if (closed) {
253-
throw new IllegalStateException("Cache has been closed");
254-
}
255-
expungeStaleEntries();
260+
expungeStaleEntries(true);
256261
map.compute(key, (k, v) -> compute00(key, mappingFunction, true));
257262
}
258263

@@ -267,9 +272,6 @@ private void compute0(K key, Function<K, V> mappingFunction) {
267272
public V putIfAbsent(K key, V value) {
268273
Objects.requireNonNull(key, "key cannot be null");
269274
Objects.requireNonNull(value, "value cannot be null");
270-
if (closed) {
271-
throw new IllegalStateException("Cache has been closed");
272-
}
273275
V existing = get(key);
274276
if (existing == null) {
275277
put(key, value);
@@ -299,10 +301,7 @@ public V putIfAbsent(K key, V value) {
299301
public V computeIfAbsent(K key, Function<K, V> mappingFunction) {
300302
Objects.requireNonNull(key, "key cannot be null");
301303
Objects.requireNonNull(mappingFunction, "mappingFunction cannot be null");
302-
if (closed) {
303-
throw new IllegalStateException("Cache has been closed");
304-
}
305-
expungeStaleEntries();
304+
expungeStaleEntries(false); // is is read or write? We don't know, so let's say read
306305

307306
// Atomically check if the key is already associated with a value.
308307
// If so, check if the value is empty. In this case, treat as if the value
@@ -389,9 +388,6 @@ public final V removeIfEmpty(K key) {
389388

390389
private V removeIfEmpty(K key, boolean performHousekeeping) {
391390
Objects.requireNonNull(key, "key cannot be null");
392-
if (closed) {
393-
throw new IllegalStateException("Cache has been closed");
394-
}
395391
SoftValue<K, V> softValue =
396392
map.computeIfPresent(
397393
key,
@@ -420,38 +416,18 @@ public synchronized void clear() {
420416
hardCache.clear();
421417
}
422418

423-
/**
424-
* Closes the cache and relinquishes any resources it holds. This includes background thread.
425-
*
426-
* <p>The cache must <i>NOT</i> be used after it has been closed. Doing so will result in an
427-
* {@link IllegalStateException}.
428-
*
429-
* @implNote The background reaper thread is a daemon thread, so it will not prevent the JVM from
430-
* shutting down. However, it is still good practice to close the cache when it is no longer
431-
* needed. Otherwise, the background reaper thread will continue to be scheduled periodically
432-
* and although it will be a no-op when it runs, it is still a waste of resources.
433-
*/
434-
@Override
435-
public void close() {
436-
if (closed) {
437-
return;
438-
}
439-
this.closed = true;
440-
clear();
441-
}
442-
443419
/**
444420
* Gets the size of the cache.
445421
*
446422
* <p>The method has constant time characteristics, <i>O(1)</i>.
447423
*/
448424
public int size() {
449-
expungeStaleEntries();
425+
expungeStaleEntries(true);
450426
return map.size();
451427
}
452428

453429
public boolean isEmpty() {
454-
expungeStaleEntries();
430+
expungeStaleEntries(true);
455431
return map.isEmpty();
456432
}
457433

@@ -466,10 +442,7 @@ public boolean isEmpty() {
466442
* @return iterator
467443
*/
468444
public Iterator<KeyValuePair<K, V>> iterator() {
469-
if (closed) {
470-
throw new IllegalStateException("Cache has been closed");
471-
}
472-
expungeStaleEntries();
445+
expungeStaleEntries(true);
473446

474447
final Iterator<SoftValue<K, V>> baseIterator = map.values().iterator();
475448

0 commit comments

Comments
 (0)