| /* | |
| * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test | |
| * @bug 8351045 8351996 8358535 | |
| * @summary tests for class-specific values | |
| * @modules java.base/java.lang:+open | |
| * @library /test/lib | |
| * @run junit ClassValueTest | |
| */ | |
| import java.lang.classfile.ClassFile; | |
| import java.lang.constant.ClassDesc; | |
| import java.lang.invoke.MethodHandle; | |
| import java.lang.invoke.MethodHandles; | |
| import java.lang.invoke.MethodType; | |
| import java.lang.ref.WeakReference; | |
| import java.time.Duration; | |
| import java.time.temporal.ChronoUnit; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.List; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.ThreadLocalRandom; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import jdk.test.lib.util.ForceGC; | |
| import org.junit.jupiter.api.Disabled; | |
| import org.junit.jupiter.api.Test; | |
| import static org.junit.jupiter.api.Assertions.*; | |
| /** | |
| * @author jrose | |
| */ | |
| final class ClassValueTest { | |
| static String nameForCV1(Class<?> type) { | |
| return "CV1:" + type.getName(); | |
| } | |
| int countForCV1; | |
| final ClassValue<String> CV1 = new CV1(); | |
| private class CV1 extends ClassValue<String> { | |
| protected String computeValue(Class<?> type) { | |
| countForCV1++; | |
| return nameForCV1(type); | |
| } | |
| } | |
| static final Class<?>[] CLASSES = { | |
| String.class, | |
| Integer.class, | |
| int.class, | |
| boolean[].class, | |
| char[][].class, | |
| ClassValueTest.class | |
| }; | |
| @Test | |
| public void testGet() { | |
| countForCV1 = 0; | |
| for (Class<?> c : CLASSES) { | |
| assertEquals(nameForCV1(c), CV1.get(c)); | |
| } | |
| assertEquals(CLASSES.length, countForCV1); | |
| for (Class<?> c : CLASSES) { | |
| assertEquals(nameForCV1(c), CV1.get(c)); | |
| } | |
| assertEquals(CLASSES.length, countForCV1); | |
| } | |
| @Test | |
| public void testRemove() { | |
| for (Class<?> c : CLASSES) { | |
| CV1.get(c); | |
| } | |
| countForCV1 = 0; | |
| int REMCOUNT = 3; | |
| for (int i = 0; i < REMCOUNT; i++) { | |
| CV1.remove(CLASSES[i]); | |
| } | |
| assertEquals(0, countForCV1); // no change | |
| for (Class<?> c : CLASSES) { | |
| assertEquals(nameForCV1(c), CV1.get(c)); | |
| } | |
| assertEquals(REMCOUNT, countForCV1); | |
| } | |
| static String nameForCVN(Class<?> type, int n) { | |
| return "CV[" + n + "]" + type.getName(); | |
| } | |
| int countForCVN; | |
| class CVN extends ClassValue<String> { | |
| final int n; | |
| CVN(int n) { this.n = n; } | |
| protected String computeValue(Class<?> type) { | |
| countForCVN++; | |
| return nameForCVN(type, n); | |
| } | |
| }; | |
| @Test | |
| public void testGetMany() { | |
| int CVN_COUNT1 = 100, CVN_COUNT2 = 100; | |
| CVN cvns[] = new CVN[CVN_COUNT1 * CVN_COUNT2]; | |
| for (int n = 0; n < cvns.length; n++) { | |
| cvns[n] = new CVN(n); | |
| } | |
| countForCVN = 0; | |
| for (int pass = 0; pass <= 2; pass++) { | |
| for (int i1 = 0; i1 < CVN_COUNT1; i1++) { | |
| eachClass: | |
| for (Class<?> c : CLASSES) { | |
| for (int i2 = 0; i2 < CVN_COUNT2; i2++) { | |
| int n = i1*CVN_COUNT2 + i2; | |
| assertEquals(0, countForCVN); | |
| assertEquals(nameForCVN(c, n), cvns[n].get(c)); | |
| cvns[n].get(c); //get it again | |
| //System.out.println("getting "+n+":"+cvns[n].get(c)); | |
| boolean doremove = (((i1 + i2) & 3) == 0); | |
| switch (pass) { | |
| case 0: | |
| assertEquals(1, countForCVN); | |
| break; | |
| case 1: | |
| // remove on middle pass | |
| assertEquals(0, countForCVN); | |
| if (doremove) { | |
| //System.out.println("removing "+n+":"+cvns[n].get(c)); | |
| cvns[n].remove(c); | |
| assertEquals(0, countForCVN); | |
| } | |
| break; | |
| case 2: | |
| assertEquals(doremove ? 1 : 0, countForCVN); | |
| break; | |
| } | |
| countForCVN = 0; | |
| if (i1 > i2 && i1 < i2+5) continue eachClass; // leave diagonal gap | |
| } | |
| } | |
| } | |
| } | |
| assertEquals(0, countForCVN); | |
| System.out.println("[rechecking values]"); | |
| for (int i = 0; i < cvns.length * 10; i++) { | |
| int n = i % cvns.length; | |
| for (Class<?> c : CLASSES) { | |
| assertEquals(nameForCVN(c, n), cvns[n].get(c)); | |
| } | |
| } | |
| } | |
| private static final long COMPUTE_TIME_MILLIS = 100; | |
| // Adjust this timeout to fail faster for test stalls | |
| private static final Duration TIMEOUT = Duration.ofNanos((long) ( | |
| Duration.of(1, ChronoUnit.MINUTES).toNanos() | |
| * Double.parseDouble(System.getProperty("test.timeout.factor", "1.0")))); | |
| private static void await(CountDownLatch latch) { | |
| try { | |
| if (!latch.await(TIMEOUT.toNanos(), TimeUnit.NANOSECONDS)) { | |
| fail("No signal received"); | |
| } | |
| } catch (InterruptedException e) { | |
| fail(e); | |
| } | |
| } | |
| private static void awaitThreads(Thread... threads) { | |
| awaitThreads(Arrays.asList(threads)); | |
| } | |
| private static void awaitThreads(Iterable<Thread> threads) { | |
| for (var t : threads) { | |
| try { | |
| if (!t.join(TIMEOUT)) { | |
| fail("Thread not stopping " + t); | |
| } | |
| } catch (InterruptedException ex) { | |
| fail(ex); | |
| } | |
| } | |
| } | |
| /** | |
| * Tests that get() + remove() can prevent stale value from being installed. | |
| * Uses junit to do basic stress. | |
| */ | |
| @Test | |
| void testRemoveStale() throws InterruptedException { | |
| CountDownLatch oldInputUsed = new CountDownLatch(1); | |
| CountDownLatch inputUpdated = new CountDownLatch(1); | |
| AtomicInteger input = new AtomicInteger(0); | |
| ClassValue<Integer> cv = new ClassValue<>() { | |
| @Override | |
| protected Integer computeValue(Class<?> type) { | |
| // must get early to represent using outdated input | |
| int v = input.get(); | |
| oldInputUsed.countDown(); | |
| // ensure input is updated when we return | |
| await(inputUpdated); | |
| return v; | |
| } | |
| }; | |
| var innocuous = Thread.startVirtualThread(() -> cv.get(int.class)); | |
| var refreshInput = Thread.startVirtualThread(() -> { | |
| await(oldInputUsed); | |
| input.incrementAndGet(); | |
| cv.remove(int.class); // Let's recompute with updated inputs! | |
| inputUpdated.countDown(); | |
| }); | |
| awaitThreads(innocuous, refreshInput); | |
| assertEquals(1, input.get(), "input not updated"); | |
| assertEquals(1, cv.get(int.class), "CV not using up-to-date input"); | |
| } | |
| /** | |
| * Tests that calling get() from computeValue() terminates. | |
| */ | |
| @Test | |
| void testGetInCompute() { | |
| ClassValue<Object> cv = new ClassValue<>() { | |
| @Override | |
| protected Object computeValue(Class<?> type) { | |
| get(type); | |
| get(type); | |
| get(type); | |
| return Boolean.TRUE; | |
| } | |
| }; | |
| try { | |
| cv.get(int.class); | |
| } catch (Throwable ex) { | |
| // swallow if any | |
| } | |
| } | |
| /** | |
| * Tests that calling remove() from computeValue() terminates. | |
| */ | |
| @Test | |
| void testRemoveInCompute() { | |
| ClassValue<Boolean> cv = new ClassValue<>() { | |
| @Override | |
| protected Boolean computeValue(Class<?> type) { | |
| remove(type); | |
| remove(type); | |
| remove(type); | |
| return Boolean.TRUE; | |
| } | |
| }; | |
| assertTrue(cv.get(int.class)); | |
| } | |
| private static Class<?> createWeakClass() { | |
| var bytes = ClassFile.of().build(ClassDesc.of("WeakTest"), _ -> {}); | |
| try { | |
| return MethodHandles.lookup().defineHiddenClass(bytes, true).lookupClass(); | |
| } catch (IllegalAccessException ex) { | |
| return fail(ex); | |
| } | |
| } | |
| @Test | |
| void testWeakAgainstClass() { | |
| Class<?> hidden = createWeakClass(); | |
| ClassValue<int[]> cv = new ClassValue<>() { | |
| @Override | |
| protected int[] computeValue(Class<?> type) { | |
| return new int[23]; | |
| } | |
| }; | |
| WeakReference<?> ref = new WeakReference<>(cv.get(hidden)); | |
| hidden = null; // Remove reference for interpreter | |
| if (!ForceGC.wait(() -> ref.refersTo(null))) { | |
| fail("Timeout"); | |
| } | |
| } | |
| @Test | |
| @Disabled // JDK-8352622 | |
| void testWeakAgainstClassValue() { | |
| ClassValue<int[]> cv = new ClassValue<>() { | |
| @Override | |
| protected int[] computeValue(Class<?> type) { | |
| return new int[23]; | |
| } | |
| }; | |
| WeakReference<?> ref = new WeakReference<>(cv.get(int.class)); | |
| cv = null; // Remove reference for interpreter | |
| if (!ForceGC.wait(() -> { | |
| CV1.get(int.class); // flush the weak maps | |
| return ref.refersTo(null); | |
| })) { | |
| fail("Timeout"); | |
| } | |
| } | |
| @Test | |
| void testSingletonWinner() { | |
| CountDownLatch raceStart = new CountDownLatch(1); | |
| ClassValue<int[]> cv = new ClassValue<>() { | |
| @Override | |
| protected int[] computeValue(Class<?> type) { | |
| await(raceStart); | |
| return new int[] {ThreadLocalRandom.current().nextInt()}; | |
| } | |
| }; | |
| AtomicReference<int[]> truth = new AtomicReference<>(null); | |
| AtomicInteger truthSwapCount = new AtomicInteger(0); | |
| List<Thread> threads = new ArrayList<>(100); | |
| Runnable job = () -> { | |
| var res = cv.get(ClassValueTest.class); | |
| var item = truth.compareAndExchange(null, res); | |
| if (item != null) { | |
| assertSame(item, res); | |
| } else { | |
| truthSwapCount.incrementAndGet(); | |
| } | |
| }; | |
| for (int i = 0; i < 100; i++) { | |
| threads.add(Thread.startVirtualThread(job)); | |
| } | |
| raceStart.countDown(); | |
| awaitThreads(threads); | |
| assertEquals(1, truthSwapCount.get()); | |
| } | |
| @Test | |
| void testRacyRemoveInCompute() { | |
| ClassValue<Object> cv = new ClassValue<>() { | |
| @Override | |
| protected Object computeValue(Class<?> type) { | |
| remove(type); | |
| try { | |
| Thread.sleep(COMPUTE_TIME_MILLIS); | |
| } catch (InterruptedException ex) { | |
| throw new RuntimeException(ex); | |
| } | |
| remove(type); | |
| return Boolean.TRUE; | |
| } | |
| }; | |
| var threads = Arrays.stream(CLASSES) | |
| .map(clz -> Thread.startVirtualThread(() -> cv.get(clz))) | |
| .toList(); | |
| awaitThreads(threads); | |
| } | |
| @Test | |
| void testRecursiveInitialization() { | |
| record Holder() { | |
| static final ClassValue<Object> clv = new ClassValue<>() { | |
| @Override | |
| protected Object computeValue(Class<?> type) { | |
| return new One(); | |
| } | |
| }; | |
| record One() { | |
| static { | |
| Holder.clv.get(One.class); | |
| } | |
| } | |
| } | |
| Holder.clv.get(Holder.One.class); | |
| } | |
| private static final ScopedValue<Integer> THREAD_ID = ScopedValue.newInstance(); | |
| @Test | |
| void testNoRecomputeOnUnrelatedRemoval() throws InterruptedException { | |
| CountDownLatch t1Started = new CountDownLatch(1); | |
| CountDownLatch removeTamper = new CountDownLatch(1); | |
| CountDownLatch t2Started = new CountDownLatch(1); | |
| CountDownLatch t1Returned = new CountDownLatch(1); | |
| AtomicInteger t1Tries = new AtomicInteger(); | |
| ClassValue<Object> clv = new ClassValue<>() { | |
| @Override | |
| protected Object computeValue(Class<?> type) { | |
| int id = THREAD_ID.get(); | |
| if (id == 1) { | |
| t1Tries.incrementAndGet(); | |
| t1Started.countDown(); | |
| await(t2Started); // implies unrelated changes | |
| } else if (id == 2) { | |
| t2Started.countDown(); | |
| // Don't race to install - we want to check computeValue tampers | |
| await(t1Returned); | |
| } else if (id != -1) { | |
| fail("Id: " + id); | |
| } | |
| return ""; | |
| } | |
| }; | |
| ScopedValue.where(THREAD_ID, -1).run(() -> clv.get(long.class)); // set up unrelated class | |
| var t1 = Thread.startVirtualThread(() -> | |
| ScopedValue.where(THREAD_ID, 1).run(() -> { | |
| clv.get(int.class); | |
| t1Returned.countDown(); // returned after x calls to computeValue | |
| })); | |
| var t2 = Thread.startVirtualThread(() -> | |
| ScopedValue.where(THREAD_ID, 2).run(() -> { | |
| await(removeTamper); | |
| clv.get(int.class); // clv version diff from that of promise | |
| })); | |
| await(t1Started); | |
| clv.remove(long.class); | |
| removeTamper.countDown(); // removed unrelated class | |
| awaitThreads(t1, t2); | |
| assertEquals(1, t1Tries.get(), "Redundant computeValue retries"); | |
| } | |
| @Test | |
| void testNoObsoleteInstallation() throws InterruptedException { | |
| CountDownLatch slowComputationStart = new CountDownLatch(1); | |
| CountDownLatch slowComputationContinue = new CountDownLatch(1); | |
| ClassValue<Integer> clv = new ClassValue<>() { | |
| @Override | |
| protected Integer computeValue(Class<?> type) { | |
| if (!THREAD_ID.isBound()) { | |
| return 5; | |
| } | |
| int threadId = THREAD_ID.get(); | |
| if (threadId == 1) { | |
| if (slowComputationContinue.getCount() == 0) { | |
| return 42; | |
| } else { | |
| // First invocation | |
| slowComputationStart.countDown(); | |
| await(slowComputationContinue); | |
| return -1; | |
| } | |
| } else { | |
| return fail("Unknown thread " + threadId); | |
| } | |
| } | |
| }; | |
| var t = Thread.startVirtualThread(() -> ScopedValue.where(THREAD_ID, 1).run(() -> { | |
| int v = clv.get(int.class); | |
| assertEquals(42, v, "recomputed value"); | |
| })); | |
| await(slowComputationStart); | |
| assertEquals(5, clv.get(int.class), "fast computation installed value"); | |
| clv.remove(int.class); | |
| slowComputationContinue.countDown(); | |
| awaitThreads(t); | |
| assertEquals(42, clv.get(int.class), "slow computation reinstalled value"); | |
| } | |
| // ClassValue cache invalidated and not reinstated when another | |
| // unrelated entry is removed | |
| @Test | |
| public void testCacheRefresh() throws Throwable { | |
| // Setup | |
| var lookup = MethodHandles.privateLookupIn(ClassValue.class, MethodHandles.lookup()); | |
| var classValueEntryClass = Class.forName("java.lang.ClassValue$Entry"); | |
| MethodHandle getCacheCarefully = lookup.findStatic(ClassValue.class, "getCacheCarefully", | |
| MethodType.methodType(classValueEntryClass.arrayType(), Class.class)); | |
| var classValueMapClass = Class.forName("java.lang.ClassValue$ClassValueMap"); | |
| MethodHandle probeHomeLocation = lookup.findStatic(classValueMapClass, "probeHomeLocation", | |
| MethodType.methodType(classValueEntryClass, classValueEntryClass.arrayType(), ClassValue.class)); | |
| MethodHandle match = lookup.findVirtual(ClassValue.class, "match", | |
| MethodType.methodType(boolean.class, classValueEntryClass)); | |
| // Work | |
| ClassValue<?> clv = new ClassValue<>() { | |
| @Override | |
| protected String computeValue(Class<?> type) { | |
| return ""; | |
| } | |
| }; | |
| // A class that shouldn't have arbitrary values stuffing the cache | |
| var cleanClass = clv.getClass(); | |
| clv.get(cleanClass); // create cache on clean class | |
| assertTrue(checkDirectCacheMatch( | |
| getCacheCarefully, | |
| probeHomeLocation, | |
| match, | |
| clv, | |
| cleanClass | |
| )); | |
| clv.get(int.class); | |
| clv.remove(int.class); // invalidate cache on clean class | |
| assertFalse(checkDirectCacheMatch( | |
| getCacheCarefully, | |
| probeHomeLocation, | |
| match, | |
| clv, | |
| cleanClass | |
| )); | |
| clv.get(cleanClass); | |
| assertTrue(checkDirectCacheMatch( | |
| getCacheCarefully, | |
| probeHomeLocation, | |
| match, | |
| clv, | |
| cleanClass | |
| )); | |
| } | |
| private static boolean checkDirectCacheMatch( | |
| MethodHandle getCacheCarefully, | |
| MethodHandle probeHomeLocation, | |
| MethodHandle match, | |
| ClassValue<?> clv, | |
| Class<?> cl | |
| ) throws Throwable { | |
| Object cache = getCacheCarefully.invoke(cl); | |
| Object entry = probeHomeLocation.invoke(cache, clv); | |
| return (boolean) match.invoke(clv, entry); | |
| } | |
| } |