78b80150e0
Co-authored-by: Patricio Chilano Mateo <pchilanomate@openjdk.org> Co-authored-by: Alan Bateman <alanb@openjdk.org> Co-authored-by: Andrew Haley <aph@openjdk.org> Co-authored-by: Fei Yang <fyang@openjdk.org> Co-authored-by: Coleen Phillimore <coleenp@openjdk.org> Co-authored-by: Richard Reingruber <rrich@openjdk.org> Co-authored-by: Martin Doerr <mdoerr@openjdk.org> Reviewed-by: aboldtch, dholmes, coleenp, fbredberg, dlong, sspitsyn
605 lines
19 KiB
Java
605 lines
19 KiB
Java
/*
|
|
* 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<Thread> 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<Thread> 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<Arguments> 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();
|
|
}
|
|
}
|
|
}
|