Skip to content

Commit e71f327

Browse files
committed
8351045: ClassValue::remove cannot ensure computation observes up-to-date state
Reviewed-by: rriggs, jrose
1 parent cef3693 commit e71f327

File tree

2 files changed

+78
-20
lines changed

2 files changed

+78
-20
lines changed

src/java.base/share/classes/java/lang/ClassValue.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -500,15 +500,15 @@ <T> Entry<T> finishEntry(ClassValue<T> classValue, Entry<T> e) {
500500
synchronized
501501
void removeEntry(ClassValue<?> classValue) {
502502
Entry<?> e = remove(classValue.identity);
503-
if (e == null) {
504-
// Uninitialized, and no pending calls to computeValue. No change.
505-
} else if (e.isPromise()) {
506-
// State is uninitialized, with a pending call to finishEntry.
507-
// Since remove is a no-op in such a state, keep the promise
508-
// by putting it back into the map.
509-
put(classValue.identity, e);
510-
} else {
511-
// In an initialized state. Bump forward, and de-initialize.
503+
// e == null: Uninitialized, and no pending calls to computeValue.
504+
// remove(identity) didn't change anything. No change.
505+
// e.isPromise(): computeValue already used outdated values.
506+
// remove(identity) discarded the outdated computation promise.
507+
// finishEntry will retry when it discovers the promise is removed.
508+
// No cache invalidation. No further action needed.
509+
if (e != null && !e.isPromise()) {
510+
// Initialized.
511+
// Bump forward to invalidate racy-read cached entries.
512512
classValue.bumpVersion();
513513
// Make all cache elements for this guy go stale.
514514
removeStaleEntries(classValue);

test/jdk/java/lang/invoke/ClassValueTest.java

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -21,22 +21,27 @@
2121
* questions.
2222
*/
2323

24-
/* @test
24+
/*
25+
* @test
26+
* @bug 8351045
2527
* @summary tests for class-specific values
26-
* @compile ClassValueTest.java
27-
* @run testng/othervm test.java.lang.invoke.ClassValueTest
28+
* @run junit ClassValueTest
2829
*/
2930

30-
package test.java.lang.invoke;
31+
import java.util.ArrayList;
32+
import java.util.concurrent.ExecutionException;
33+
import java.util.concurrent.Executors;
34+
import java.util.concurrent.Future;
35+
import java.util.concurrent.atomic.AtomicInteger;
36+
37+
import org.junit.jupiter.api.Test;
3138

32-
import org.testng.*;
33-
import static org.testng.AssertJUnit.*;
34-
import org.testng.annotations.*;
39+
import static org.junit.jupiter.api.Assertions.assertEquals;
3540

3641
/**
3742
* @author jrose
3843
*/
39-
public class ClassValueTest {
44+
final class ClassValueTest {
4045
static String nameForCV1(Class<?> type) {
4146
return "CV1:" + type.getName();
4247
}
@@ -143,7 +148,7 @@ public void testGetMany() {
143148
}
144149
}
145150
}
146-
assertEquals(countForCVN, 0);
151+
assertEquals(0, countForCVN);
147152
System.out.println("[rechecking values]");
148153
for (int i = 0; i < cvns.length * 10; i++) {
149154
int n = i % cvns.length;
@@ -152,4 +157,57 @@ public void testGetMany() {
152157
}
153158
}
154159
}
160+
161+
private static final int RUNS = 16;
162+
private static final long COMPUTE_TIME_MILLIS = 100;
163+
164+
@Test
165+
void testRemoveOnComputeCases() {
166+
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
167+
var tasks = new ArrayList<Future<?>>(RUNS);
168+
for (int i = 0; i < RUNS; i++) {
169+
tasks.add(exec.submit(this::testRemoveOnCompute));
170+
}
171+
for (var task : tasks) {
172+
try {
173+
task.get();
174+
} catch (InterruptedException | ExecutionException ex) {
175+
var cause = ex.getCause();
176+
if (cause instanceof AssertionError ae)
177+
throw ae;
178+
throw new RuntimeException(ex);
179+
}
180+
}
181+
}
182+
}
183+
184+
void testRemoveOnCompute() {
185+
AtomicInteger input = new AtomicInteger(0);
186+
ClassValue<Integer> cv = new ClassValue<>() {
187+
@Override
188+
protected Integer computeValue(Class<?> type) {
189+
// must get early to represent using outdated input
190+
int v = input.get();
191+
try {
192+
Thread.sleep(COMPUTE_TIME_MILLIS);
193+
} catch (InterruptedException ex) {
194+
throw new RuntimeException(ex);
195+
}
196+
return v;
197+
}
198+
};
199+
var innocuous = Thread.startVirtualThread(() -> cv.get(int.class));
200+
var refreshInput = Thread.startVirtualThread(() -> {
201+
input.incrementAndGet();
202+
cv.remove(int.class); // Let's recompute with updated inputs!
203+
});
204+
try {
205+
innocuous.join();
206+
refreshInput.join();
207+
} catch (InterruptedException ex) {
208+
throw new RuntimeException(ex);
209+
}
210+
assertEquals(1, input.get(), "input not updated");
211+
assertEquals(1, cv.get(int.class), "CV not using up-to-date input");
212+
}
155213
}

0 commit comments

Comments
 (0)