| /* |
| * 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); |
| } |
| } |
| } |
| } |
| } |