blob: 06f3cf44eeee4a14b69a903a9969f99e8476a37b [file]
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Verify that {@link Thread#sleep(long)} API parks virtual threads.
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
if (!com.android.art.flags.Flags.virtualThreadImplV1()) {
return;
}
// Exit if the thread throws any exception.
Thread.setDefaultUncaughtExceptionHandler(HANDLER);
testSleep(2);
testSleep(10);
testSleep(100);
testSleep(300);
testSleepThreadPinning();
testSleepViaReflection(1);
testSleepViaReflection(10);
testSleepViaMethodHandle(1, /*isInvokeExact*/ false);
testSleepViaMethodHandle(10, /*isInvokeExact*/ false);
testSleepViaMethodHandle(1, /*isInvokeExact*/ true);
testSleepViaMethodHandle(10, /*isInvokeExact*/ true);
}
private static final Thread.UncaughtExceptionHandler HANDLER = (t, e) -> {
System.err.println("thread: " + t.getName());
e.printStackTrace(System.err);
System.exit(1);
};
private static final long SLEEP_DURATION_MS = 10;
/**
* Start {@code numOfThreads} virtual threads sleeping for the given duration and wait until
* all threads join or time out.
* @param numOfThreads number of concurrent virtual threads sleeping
*/
private static void testSleep(int numOfThreads)
throws InterruptedException {
Runnable task = () -> {
try {
Thread.sleep(SLEEP_DURATION_MS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
testSleeping(task, numOfThreads, UnmountListener.STATE_PARKED_AND_UNMOUNTED);
}
private static void testSleeping(Runnable runnable, int numOfThreads,
int expectedUnmountState) throws InterruptedException {
List<VirtualThread> threads = new ArrayList<>(numOfThreads);
for (int i = 0; i < numOfThreads; i++) {
VirtualThread vt = (VirtualThread) Thread.ofVirtual()
.uncaughtExceptionHandler(HANDLER)
.unstarted(runnable);
vt.setJvmtiEventListener(new UnmountListener(vt));
threads.add(vt);
vt.start();
}
for (VirtualThread vt : threads) {
vt.join();
}
for (VirtualThread vt : threads) {
UnmountListener listener = (UnmountListener) vt.getJvmtiEventListener();
int state = listener.state.get();
if (state != expectedUnmountState) {
throw new AssertionError("Expected state code " + expectedUnmountState
+ ", but the state code was " + state);
}
}
}
private static void testSleepThreadPinning() throws InterruptedException {
Runnable task = () -> {
try {
Thread t = Thread.currentThread();
synchronized (t) {
if (!Thread.holdsLock(t)) {
throw new AssertionError("Lock should be held");
}
Thread.sleep(SLEEP_DURATION_MS);
if (!Thread.holdsLock(t)) {
throw new AssertionError("Lock should be held");
}
}
if (Thread.holdsLock(t)) {
throw new AssertionError("Lock shouldn't be held");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
testSleeping(task, 100, UnmountListener.STATE_PARKED_AND_UNMOUNTED);
}
private static void testSleepViaReflection(int numOfThreads) throws InterruptedException {
Runnable task = () -> {
try {
Method m = Thread.class.getMethod("sleep", long.class);
m.invoke(null, SLEEP_DURATION_MS);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
};
// TODO: Unpin virtual thread from reflection frame.
// When the virtual thread terminates, it's unmounted from the carrier thread.
testSleeping(task, numOfThreads, UnmountListener.STATE_UNMOUNTED);
}
private static final MethodHandle METHOD_HANDLE_SLEEP;
private static final MethodHandle METHOD_HANDLE_SLEEP_PLUS_ONE;
static {
try {
METHOD_HANDLE_SLEEP = MethodHandles.lookup()
.findStatic(Thread.class, "sleep", MethodType.methodType(void.class, long.class));
MethodHandle plusOne = MethodHandles.lookup()
.findStatic(Main.class, "plusOne", MethodType.methodType(long.class, long.class));
METHOD_HANDLE_SLEEP_PLUS_ONE = MethodHandles.filterArguments(
METHOD_HANDLE_SLEEP, 0, plusOne);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static long plusOne(long l) {
return l + 1;
}
private static void testSleepViaMethodHandle(int numOfThreads, boolean isInvokeExact)
throws InterruptedException {
final Runnable taskSleep;
final Runnable taskCombinator;
if (isInvokeExact) {
class InvokeExactTask implements Runnable {
private final MethodHandle m;
InvokeExactTask(MethodHandle m) {
this.m = m;
}
@Override
public void run() {
try {
m.invokeExact(SLEEP_DURATION_MS);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
taskSleep = new InvokeExactTask(METHOD_HANDLE_SLEEP); // Test Thread.sleep()
taskCombinator = new InvokeExactTask(METHOD_HANDLE_SLEEP_PLUS_ONE); // Test combinator.
} else {
class InvokeTask implements Runnable {
private final MethodHandle m;
InvokeTask(MethodHandle m) {
this.m = m;
}
@Override
public void run() {
try {
m.invokeExact(SLEEP_DURATION_MS);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
taskSleep = new InvokeTask(METHOD_HANDLE_SLEEP); // Test Thread.sleep()
taskCombinator = new InvokeTask(METHOD_HANDLE_SLEEP_PLUS_ONE); // Test combinator.
}
testSleeping(taskSleep, numOfThreads, UnmountListener.STATE_PARKED_AND_UNMOUNTED);
// TODO: investigate why the thread is pinned.
testSleeping(taskCombinator, numOfThreads, UnmountListener.STATE_UNMOUNTED);
}
private static class UnmountListener implements VirtualThread.JvmtiEventsListener {
private static final int STATE_NOT_UNMOUNTED = 0;
// When the virtual thread is pinned or terminates.
private static final int STATE_UNMOUNTED = 1;
// When the virtual thread is parked, not pinned.
private static final int STATE_PARKED_AND_UNMOUNTED = 2;
private final VirtualThread vt;
// Indicate if the virtual thread is ever parked and unmounted.
private final AtomicInteger state = new AtomicInteger(STATE_NOT_UNMOUNTED);
public UnmountListener(VirtualThread vt) {
this.vt = vt;
}
@Override
public void onJvmtiUnmount(boolean hide) {
if (!hide && state.get() != STATE_PARKED_AND_UNMOUNTED) {
if (vt.getVirtualThreadContext().isUnmounted()) {
state.set(STATE_PARKED_AND_UNMOUNTED);
} else {
state.set(STATE_UNMOUNTED);
}
}
}
}
}