Skip to content

Commit cacd8e1

Browse files
committed
Use relaxed reads for key
The key fixed unless the node transitions to the retired or dead state under the node's lock. When it does, the entry should not be visible from the caller's perspective. This was audited to verify correct usages. The conditional remove was modified to follow the standard pattern of remove, then transition. It was implemented as a conditional transition followed by a remove. This was legacy from CLHM where compute() methods had not originally been available. By using computeIfPresent to conditionally remove, we have more assurance of the correct behavior. The previous may have even been wrong if the value had been weak/soft GC'd. Relaxed reads should offer a slight performance improvement due to avoiding unnecessary memory barriers.
1 parent 333b776 commit cacd8e1

File tree

5 files changed

+61
-62
lines changed

5 files changed

+61
-62
lines changed

caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeGenerator.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ private void addKey() {
132132
nodeSubtype
133133
.addField(newFieldOffset(className, "key"))
134134
.addField(newKeyField())
135-
.addMethod(newGetter(keyStrength, kTypeVar, "key", Visibility.IMMEDIATE))
136-
.addMethod(newGetterRef("key"));
135+
.addMethod(newGetter(keyStrength, kTypeVar, "key", Visibility.LAZY))
136+
.addMethod(newGetKeyRef());
137137
addKeyConstructorAssignment(constructorByKey, false);
138138
addKeyConstructorAssignment(constructorByKeyRef, true);
139139
}
@@ -361,14 +361,14 @@ private void addStateMethods() {
361361
: baseClassName() + '.' + offsetName("key");
362362

363363
nodeSubtype.addMethod(MethodSpec.methodBuilder("isAlive")
364-
.addStatement("Object key = this.key")
364+
.addStatement("Object key = getKeyReference()")
365365
.addStatement("return (key != $L) && (key != $L)", retiredArg, deadArg)
366366
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
367367
.returns(boolean.class)
368368
.build());
369369

370370
nodeSubtype.addMethod(MethodSpec.methodBuilder("isRetired")
371-
.addStatement("return (key == $L)", retiredArg)
371+
.addStatement("return (getKeyReference() == $L)", retiredArg)
372372
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
373373
.returns(boolean.class)
374374
.build());
@@ -379,7 +379,7 @@ private void addStateMethods() {
379379
.build());
380380

381381
nodeSubtype.addMethod(MethodSpec.methodBuilder("isDead")
382-
.addStatement("return (key == $L)", deadArg)
382+
.addStatement("return (getKeyReference() == $L)", deadArg)
383383
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
384384
.returns(boolean.class)
385385
.build());
@@ -397,14 +397,13 @@ private String baseClassName() {
397397
return Feature.makeClassName(keyAndValue);
398398
}
399399

400-
/** Creates an accessor that returns the reference holding the variable. */
401-
private MethodSpec newGetterRef(String varName) {
402-
String methodName = String.format("get%sReference",
403-
Character.toUpperCase(varName.charAt(0)) + varName.substring(1));
404-
MethodSpec.Builder getter = MethodSpec.methodBuilder(methodName)
400+
/** Creates an accessor that returns the key reference. */
401+
private MethodSpec newGetKeyRef() {
402+
MethodSpec.Builder getter = MethodSpec.methodBuilder("getKeyReference")
405403
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
406404
.returns(Object.class);
407-
getter.addStatement("return $N", varName);
405+
getter.addStatement("return $T.UNSAFE.getObject(this, $N)",
406+
UNSAFE_ACCESS, offsetName("key"));
408407
return getter.build();
409408
}
410409

caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
import javax.annotation.concurrent.ThreadSafe;
2727

2828
/**
29-
* A semi-persistent mapping from keys to values. Values are automatically loaded by the cache,
30-
* and are stored in the cache until either evicted or manually invalidated.
29+
* A semi-persistent mapping from keys to values. Values are automatically loaded by the cache
30+
* asynchronously, and are stored in the cache until either evicted or manually invalidated.
3131
* <p>
3232
* Implementations of this interface are expected to be thread-safe, and can be safely accessed
3333
* by multiple concurrent threads.

caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ void afterRead(Node<K, V> node, boolean recordHit) {
470470
if (((now - writeTime) > refreshAfterWriteNanos()) && node.casWriteTime(writeTime, now)) {
471471
executor().execute(() -> {
472472
K key = node.getKey();
473-
if (key != null) {
473+
if ((key != null) && node.isAlive()) {
474474
try {
475475
computeIfPresent(key, cacheLoader()::reload);
476476
} catch (Throwable t) {
@@ -943,13 +943,23 @@ public V remove(Object key) {
943943
return null;
944944
}
945945

946-
V oldValue = node.makeRetired();
947-
if (oldValue != null) {
946+
boolean retired = false;
947+
synchronized (node) {
948+
if (node.isAlive()) {
949+
retired = true;
950+
node.retire();
951+
}
952+
}
953+
V oldValue = node.getValue();
954+
if (retired) {
948955
afterWrite(node, new RemovalTask(node));
949956
if (hasRemovalListener()) {
950957
@SuppressWarnings("unchecked")
951958
K castKey = (K) key;
952-
notifyRemoval(castKey, oldValue, RemovalCause.EXPLICIT);
959+
RemovalCause cause = (oldValue == null)
960+
? RemovalCause.COLLECTED
961+
: RemovalCause.EXPLICIT;
962+
notifyRemoval(castKey, oldValue, cause);
953963
}
954964
}
955965
return oldValue;
@@ -958,26 +968,31 @@ public V remove(Object key) {
958968
@Override
959969
public boolean remove(Object key, Object value) {
960970
Object keyRef = nodeFactory.newLookupKey(key);
961-
final Node<K, V> node = data.get(keyRef);
962971
tracer().recordDelete(id, key);
963-
if ((node == null) || (value == null)) {
972+
if ((data.get(keyRef) == null) || (value == null)) {
964973
return false;
965974
}
966-
V oldValue;
967-
synchronized (node) {
968-
oldValue = node.getValue();
969-
if (node.isAlive() && node.containsValue(value)) {
970-
node.retire();
971-
} else {
972-
return false;
975+
@SuppressWarnings({"unchecked", "rawtypes"})
976+
Node<K, V> removed[] = new Node[1];
977+
data.computeIfPresent(keyRef, (k, node) -> {
978+
synchronized (node) {
979+
if (node.isAlive() && node.containsValue(value)) {
980+
node.retire();
981+
} else {
982+
return node;
983+
}
973984
}
974-
}
975-
if (data.remove(keyRef, node) && hasRemovalListener()) {
985+
removed[0] = node;
986+
return null;
987+
});
988+
if (removed[0] == null) {
989+
return false;
990+
} else if (hasRemovalListener()) {
976991
@SuppressWarnings("unchecked")
977992
K castKey = (K) key;
978-
notifyRemoval(castKey, oldValue, RemovalCause.EXPLICIT);
993+
notifyRemoval(castKey, removed[0].getValue(), RemovalCause.EXPLICIT);
979994
}
980-
afterWrite(node, new RemovalTask(node));
995+
afterWrite(removed[0], new RemovalTask(removed[0]));
981996
return true;
982997
}
983998

@@ -1128,7 +1143,7 @@ public V computeIfPresent(K key,
11281143
V oldValue = prior.getValue();
11291144
newValue[0] = statsAware(remappingFunction, false, false).apply(key, oldValue);
11301145
if (newValue[0] == null) {
1131-
prior.makeRetired();
1146+
prior.retire();
11321147
task[0] = new RemovalTask(prior);
11331148
if (hasRemovalListener()) {
11341149
notifyRemoval(key, oldValue, RemovalCause.EXPLICIT);
@@ -1312,7 +1327,7 @@ Map<K, V> orderedMap(LinkedDeque<Node<K, V>> deque, Function<V, V> transformer,
13121327
Node<K, V> node = iterator.next();
13131328
K key = node.getKey();
13141329
V value = transformer.apply(node.getValue());
1315-
if ((key != null) && (value != null)) {
1330+
if ((key != null) && (value != null) && node.isAlive()) {
13161331
map.put(key, value);
13171332
}
13181333
}
@@ -1537,7 +1552,7 @@ public boolean hasNext() {
15371552
next = iterator.next();
15381553
value = next.getValue();
15391554
key = next.getKey();
1540-
if (hasExpired(next, now) || (key == null) || (value == null)) {
1555+
if (hasExpired(next, now) || (key == null) || (value == null) || !next.isAlive()) {
15411556
value = null;
15421557
next = null;
15431558
key = null;
@@ -1808,7 +1823,7 @@ private Map<K, V> sortedByWriteTime(boolean ascending, int limit) {
18081823
Node<K, V> node = iterator.next();
18091824
K key = node.getKey();
18101825
V value = transformer.apply(node.getValue());
1811-
if ((key != null) && (value != null)) {
1826+
if ((key != null) && (value != null) && node.isAlive()) {
18121827
map.put(key, value);
18131828
}
18141829
}

caffeine/src/main/java/com/github/benmanes/caffeine/cache/Node.java

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,6 @@ interface Node<K, V> extends AccessOrder<Node<K, V>>, WriteOrder<Node<K, V>> {
7373
/** If the entry was removed from the hash-table and the page replacement policy. */
7474
boolean isDead();
7575

76-
/**
77-
* Atomically transitions the node from the <tt>alive</tt> state to the <tt>retired</tt> state, if
78-
* a valid transition.
79-
*
80-
* @return the retired weighted value if the transition was successful or null otherwise
81-
*/
82-
default @Nullable V makeRetired() {
83-
synchronized (this) {
84-
if (!isAlive()) {
85-
return null;
86-
}
87-
retire();
88-
return getValue();
89-
}
90-
}
91-
9276
/** Sets the node to the <tt>retired</tt> state. */
9377
@GuardedBy("this")
9478
void retire();

caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,21 @@ public void evict_alreadyRemoved(Cache<Integer, Integer> cache, CacheContext con
108108
try {
109109
Object keyRef = localCache.nodeFactory.newLookupKey(oldEntry.getKey());
110110
Node<Integer, Integer> node = localCache.data.get(keyRef);
111-
checkStatus(localCache, node, Status.ALIVE);
111+
checkStatus(node, Status.ALIVE);
112112
ConcurrentTestHarness.execute(() -> {
113113
localCache.put(newEntry.getKey(), newEntry.getValue());
114114
assertThat(localCache.remove(oldEntry.getKey()), is(oldEntry.getValue()));
115115
});
116116
Awaits.await().until(() -> localCache.containsKey(oldEntry.getKey()), is(false));
117-
checkStatus(localCache, node, Status.RETIRED);
117+
Awaits.await().until(() -> {
118+
synchronized (node) {
119+
return !node.isAlive();
120+
}
121+
});
122+
checkStatus(node, Status.RETIRED);
118123
localCache.drainBuffers();
119124

120-
checkStatus(localCache, node, Status.DEAD);
125+
checkStatus(node, Status.DEAD);
121126
assertThat(localCache.containsKey(newEntry.getKey()), is(true));
122127
assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.EXPLICIT));
123128
} finally {
@@ -127,15 +132,11 @@ public void evict_alreadyRemoved(Cache<Integer, Integer> cache, CacheContext con
127132

128133
enum Status { ALIVE, RETIRED, DEAD }
129134

130-
static void checkStatus(BoundedLocalCache<Integer, Integer> localCache,
131-
Node<Integer, Integer> node, Status expected) {
132-
assertThat(node.isAlive(), is(expected == Status.ALIVE));
133-
assertThat(node.isRetired(), is(expected == Status.RETIRED));
134-
assertThat(node.isDead(), is(expected == Status.DEAD));
135-
136-
if (node.isDead()) {
137-
node.makeRetired();
138-
assertThat(node.isRetired(), is(false));
135+
static void checkStatus(Node<Integer, Integer> node, Status expected) {
136+
synchronized (node) {
137+
assertThat(node.isAlive(), is(expected == Status.ALIVE));
138+
assertThat(node.isRetired(), is(expected == Status.RETIRED));
139+
assertThat(node.isDead(), is(expected == Status.DEAD));
139140
}
140141
}
141142

0 commit comments

Comments
 (0)