/* * Copyright (c) 2023, 2024, 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 id=default * @summary Test virtual thread with monitor enter/exit * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=LM_LEGACY * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=LM_LIGHTWEIGHT * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xint-LM_LEGACY * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xint-LM_LIGHTWEIGHT * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xcomp-LM_LEGACY * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xcomp -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xcomp-LM_LIGHTWEIGHT * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xcomp -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xcomp-TieredStopAtLevel1-LM_LIGHTWEIGHT * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xcomp-noTieredCompilation-LM_LEGACY * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ /* * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT * @modules java.base/java.lang:+open jdk.management * @library /test/lib * @build LockingMode * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit */ import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.LockSupport; import java.util.stream.IntStream; import java.util.stream.Stream; import jdk.test.lib.thread.VThreadPinner; import jdk.test.lib.thread.VThreadRunner; // ensureParallelism requires jdk.management import jdk.test.lib.thread.VThreadScheduler; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; class MonitorEnterExit { static final int MAX_VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors(); static final int MAX_ENTER_DEPTH = 256; @BeforeAll static void setup() { // need >=2 carriers for tests that pin VThreadRunner.ensureParallelism(2); } /** * Test monitor enter with no contention. */ @Test void testEnterNoContention() throws Exception { var lock = new Object(); VThreadRunner.run(() -> { synchronized (lock) { assertTrue(Thread.holdsLock(lock)); } assertFalse(Thread.holdsLock(lock)); }); } /** * Test monitor enter with contention, monitor is held by platform thread. */ @Test void testEnterWhenHeldByPlatformThread() throws Exception { testEnterWithContention(); } /** * Test monitor enter with contention, monitor is held by virtual thread. */ @Test void testEnterWhenHeldByVirtualThread() throws Exception { VThreadRunner.run(this::testEnterWithContention); } /** * Test monitor enter with contention, monitor will be held by caller thread. */ private void testEnterWithContention() throws Exception { var lock = new Object(); var started = new CountDownLatch(1); var entered = new AtomicBoolean(); var vthread = Thread.ofVirtual().unstarted(() -> { started.countDown(); synchronized (lock) { assertTrue(Thread.holdsLock(lock)); entered.set(true); } assertFalse(Thread.holdsLock(lock)); }); try { synchronized (lock) { vthread.start(); // wait for thread to start and block started.await(); await(vthread, Thread.State.BLOCKED); assertFalse(entered.get()); } } finally { vthread.join(); } assertTrue(entered.get()); } /** * Test monitor reenter. */ @Test void testReenter() throws Exception { var lock = new Object(); VThreadRunner.run(() -> { testReenter(lock, 0); assertFalse(Thread.holdsLock(lock)); }); } private void testReenter(Object lock, int depth) { if (depth < MAX_ENTER_DEPTH) { synchronized (lock) { assertTrue(Thread.holdsLock(lock)); testReenter(lock, depth + 1); assertTrue(Thread.holdsLock(lock)); } } } /** * Test monitor reenter when there are other threads blocked trying to enter. */ @Test @DisabledIf("LockingMode#isLegacy") void testReenterWithContention() throws Exception { var lock = new Object(); VThreadRunner.run(() -> { List threads = new ArrayList<>(); testReenter(lock, 0, threads); // wait for threads to terminate for (Thread vthread : threads) { vthread.join(); } }); } private void testReenter(Object lock, int depth, List threads) throws Exception { if (depth < MAX_ENTER_DEPTH) { synchronized (lock) { assertTrue(Thread.holdsLock(lock)); // start platform or virtual thread that blocks waiting to enter var started = new CountDownLatch(1); ThreadFactory factory = ThreadLocalRandom.current().nextBoolean() ? Thread.ofPlatform().factory() : Thread.ofVirtual().factory(); var thread = factory.newThread(() -> { started.countDown(); synchronized (lock) { /* do nothing */ } }); thread.start(); // wait for thread to start and block started.await(); await(thread, Thread.State.BLOCKED); threads.add(thread); // test reenter testReenter(lock, depth + 1, threads); } } } /** * Test monitor enter when pinned. */ @Test void testEnterWhenPinned() throws Exception { var lock = new Object(); VThreadPinner.runPinned(() -> { synchronized (lock) { assertTrue(Thread.holdsLock(lock)); } assertFalse(Thread.holdsLock(lock)); }); } /** * Test monitor reenter when pinned. */ @Test void testReenterWhenPinned() throws Exception { VThreadRunner.run(() -> { var lock = new Object(); synchronized (lock) { VThreadPinner.runPinned(() -> { assertTrue(Thread.holdsLock(lock)); synchronized (lock) { assertTrue(Thread.holdsLock(lock)); } assertTrue(Thread.holdsLock(lock)); }); } assertFalse(Thread.holdsLock(lock)); }); } /** * Test contended monitor enter when pinned. Monitor is held by platform thread. */ @Test void testContendedEnterWhenPinnedHeldByPlatformThread() throws Exception { testEnterWithContentionWhenPinned(); } /** * Test contended monitor enter when pinned. Monitor is held by virtual thread. */ @Test void testContendedEnterWhenPinnedHeldByVirtualThread() throws Exception { VThreadRunner.run(this::testEnterWithContentionWhenPinned); } /** * Test contended monitor enter when pinned, monitor will be held by caller thread. */ private void testEnterWithContentionWhenPinned() throws Exception { var lock = new Object(); var started = new CountDownLatch(1); var entered = new AtomicBoolean(); Thread vthread = Thread.ofVirtual().unstarted(() -> { VThreadPinner.runPinned(() -> { started.countDown(); synchronized (lock) { entered.set(true); } }); }); synchronized (lock) { // start thread and wait for it to block vthread.start(); started.await(); await(vthread, Thread.State.BLOCKED); assertFalse(entered.get()); } vthread.join(); // check thread entered monitor assertTrue(entered.get()); } /** * Test that blocking waiting to enter a monitor releases the carrier. */ @Test @DisabledIf("LockingMode#isLegacy") void testReleaseWhenBlocked() throws Exception { assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); var lock = new Object(); // thread enters monitor var started = new CountDownLatch(1); var vthread1 = factory.newThread(() -> { started.countDown(); synchronized (lock) { } }); try { synchronized (lock) { // start thread and wait for it to block vthread1.start(); started.await(); await(vthread1, Thread.State.BLOCKED); // carrier should be released, use it for another thread var executed = new AtomicBoolean(); var vthread2 = factory.newThread(() -> { executed.set(true); }); vthread2.start(); vthread2.join(); assertTrue(executed.get()); } } finally { vthread1.join(); } } } /** * Test lots of virtual threads blocked waiting to enter a monitor. If the number * of virtual threads exceeds the number of carrier threads this test will hang if * carriers aren't released. */ @Test @DisabledIf("LockingMode#isLegacy") void testManyBlockedThreads() throws Exception { Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT]; var lock = new Object(); synchronized (lock) { for (int i = 0; i < MAX_VTHREAD_COUNT; i++) { var started = new CountDownLatch(1); var vthread = Thread.ofVirtual().start(() -> { started.countDown(); synchronized (lock) { } }); // wait for thread to start and block started.await(); await(vthread, Thread.State.BLOCKED); vthreads[i] = vthread; } } // cleanup for (int i = 0; i < MAX_VTHREAD_COUNT; i++) { vthreads[i].join(); } } /** * Returns a stream of elements that are ordered pairs of platform and virtual thread * counts. 0,2,4,..16 platform threads. 2,4,6,..32 virtual threads. */ static Stream threadCounts() { return IntStream.range(0, 17) .filter(i -> i % 2 == 0) .mapToObj(i -> i) .flatMap(np -> IntStream.range(2, 33) .filter(i -> i % 2 == 0) .mapToObj(vp -> Arguments.of(np, vp))); } /** * Test mutual exclusion of monitors with platform and virtual threads. */ @ParameterizedTest @MethodSource("threadCounts") void testMutualExclusion(int nPlatformThreads, int nVirtualThreads) throws Exception { class Counter { int count; synchronized void increment() { count++; Thread.yield(); } } var counter = new Counter(); int nThreads = nPlatformThreads + nVirtualThreads; var threads = new Thread[nThreads]; int index = 0; for (int i = 0; i < nPlatformThreads; i++) { threads[index] = Thread.ofPlatform() .name("platform-" + index) .unstarted(counter::increment); index++; } for (int i = 0; i < nVirtualThreads; i++) { threads[index] = Thread.ofVirtual() .name("virtual-" + index) .unstarted(counter::increment); index++; } // start all threads for (Thread thread : threads) { thread.start(); } // wait for all threads to terminate for (Thread thread : threads) { thread.join(); } assertEquals(nThreads, counter.count); } /** * Test unblocking a virtual thread waiting to enter a monitor held by a platform thread. */ @RepeatedTest(20) void testUnblockingByPlatformThread() throws Exception { testUnblocking(); } /** * Test unblocking a virtual thread waiting to enter a monitor held by another * virtual thread. */ @RepeatedTest(20) void testUnblockingByVirtualThread() throws Exception { VThreadRunner.run(this::testUnblocking); } /** * Test unblocking a virtual thread waiting to enter a monitor, monitor will be * initially be held by caller thread. */ private void testUnblocking() throws Exception { var lock = new Object(); var started = new CountDownLatch(1); var entered = new AtomicBoolean(); var vthread = Thread.ofVirtual().unstarted(() -> { started.countDown(); synchronized (lock) { entered.set(true); } }); try { synchronized (lock) { vthread.start(); started.await(); // random delay before exiting monitor switch (ThreadLocalRandom.current().nextInt(4)) { case 0 -> { /* no delay */} case 1 -> Thread.onSpinWait(); case 2 -> Thread.yield(); case 3 -> await(vthread, Thread.State.BLOCKED); default -> fail(); } assertFalse(entered.get()); } } finally { vthread.join(); } assertTrue(entered.get()); } /** * Test that unblocking a virtual thread waiting to enter a monitor does not consume * the thread's parking permit. */ @Test void testParkingPermitNotConsumed() throws Exception { var lock = new Object(); var started = new CountDownLatch(1); var vthread = Thread.ofVirtual().unstarted(() -> { started.countDown(); LockSupport.unpark(Thread.currentThread()); synchronized (lock) { } // should block LockSupport.park(); // should not park }); synchronized (lock) { vthread.start(); // wait for thread to start and block started.await(); await(vthread, Thread.State.BLOCKED); } vthread.join(); } /** * Test that unblocking a virtual thread waiting to enter a monitor does not make * available the thread's parking permit. */ @Test void testParkingPermitNotOffered() throws Exception { var lock = new Object(); var started = new CountDownLatch(1); var vthread = Thread.ofVirtual().unstarted(() -> { started.countDown(); synchronized (lock) { } // should block LockSupport.park(); // should park }); synchronized (lock) { vthread.start(); // wait for thread to start and block started.await(); await(vthread, Thread.State.BLOCKED); } try { // wait for thread to park, it should not terminate await(vthread, Thread.State.WAITING); vthread.join(Duration.ofMillis(100)); assertEquals(Thread.State.WAITING, vthread.getState()); } finally { LockSupport.unpark(vthread); vthread.join(); } } /** * Waits for the given thread to reach a given state. */ private void await(Thread thread, Thread.State expectedState) throws InterruptedException { Thread.State state = thread.getState(); while (state != expectedState) { assertTrue(state != Thread.State.TERMINATED, "Thread has terminated"); Thread.sleep(10); state = thread.getState(); } } }