1 /* 2 * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /** 25 * @test 26 * @summary Test using a custom scheduler as the default virtual thread scheduler 27 * @requires vm.continuations 28 * @run junit/othervm -Djdk.virtualThreadScheduler.implClass=CustomDefaultScheduler$CustomScheduler1 29 * --enable-native-access=ALL-UNNAMED CustomDefaultScheduler 30 * @run junit/othervm -Djdk.virtualThreadScheduler.implClass=CustomDefaultScheduler$CustomScheduler2 31 * --enable-native-access=ALL-UNNAMED CustomDefaultScheduler 32 */ 33 34 import java.lang.Thread.VirtualThreadScheduler; 35 import java.util.concurrent.Executor; 36 import java.util.concurrent.Executors; 37 import java.util.concurrent.ExecutorService; 38 import java.util.concurrent.ThreadFactory; 39 import java.util.concurrent.CountDownLatch; 40 import java.util.concurrent.atomic.AtomicBoolean; 41 import java.util.concurrent.atomic.AtomicReference; 42 import java.util.concurrent.locks.LockSupport; 43 44 import org.junit.jupiter.api.Test; 45 import org.junit.jupiter.api.BeforeAll; 46 import static org.junit.jupiter.api.Assertions.*; 47 import static org.junit.jupiter.api.Assumptions.*; 48 49 class CustomDefaultScheduler { 50 private static String schedulerClassName; 51 52 @BeforeAll 53 static void setup() { 54 schedulerClassName = System.getProperty("jdk.virtualThreadScheduler.implClass"); 55 } 56 57 /** 58 * Custom scheduler that uses a thread pool. 59 */ 60 public static class CustomScheduler1 implements VirtualThreadScheduler { 61 private final ExecutorService pool; 62 63 public CustomScheduler1() { 64 ThreadFactory factory = Thread.ofPlatform().daemon().factory(); 65 pool = Executors.newFixedThreadPool(1, factory); 66 } 67 68 @Override 69 public void execute(Thread thread, Runnable task) { 70 if (thread.isVirtual()) { 71 pool.execute(task); 72 } else { 73 throw new UnsupportedOperationException(); 74 } 75 } 76 } 77 78 /** 79 * Custom scheduler that delegates to the built-in default scheduler. 80 */ 81 public static class CustomScheduler2 implements VirtualThreadScheduler { 82 private final VirtualThreadScheduler builtinScheduler; 83 84 public CustomScheduler2(VirtualThreadScheduler builtinScheduler) { 85 this.builtinScheduler = builtinScheduler; 86 } 87 88 VirtualThreadScheduler builtinScheduler() { 89 return builtinScheduler; 90 } 91 92 @Override 93 public void execute(Thread vthread, Runnable task) { 94 builtinScheduler.execute(vthread, task); 95 } 96 } 97 98 /** 99 * Test that a virtual thread uses the custom default scheduler. 100 */ 101 @Test 102 void testUseCustomScheduler() throws Exception { 103 var ref = new AtomicReference<VirtualThreadScheduler>(); 104 Thread.startVirtualThread(() -> { 105 ref.set(VirtualThreadScheduler.current()); 106 }).join(); 107 VirtualThreadScheduler scheduler = ref.get(); 108 assertEquals(schedulerClassName, scheduler.getClass().getName()); 109 } 110 111 /** 112 * Test virtual thread park/unpark using custom default scheduler. 113 */ 114 @Test 115 void testPark() throws Exception { 116 var done = new AtomicBoolean(); 117 var thread = Thread.startVirtualThread(() -> { 118 while (!done.get()) { 119 LockSupport.park(); 120 } 121 }); 122 try { 123 await(thread, Thread.State.WAITING); 124 } finally { 125 done.set(true); 126 LockSupport.unpark(thread); 127 thread.join(); 128 } 129 } 130 131 /** 132 * Test virtual thread blocking on monitor when using custom default scheduler. 133 */ 134 @Test 135 void testBlock() throws Exception { 136 var ready = new CountDownLatch(1); 137 var lock = new Object(); 138 var thread = Thread.ofVirtual().unstarted(() -> { 139 ready.countDown(); 140 synchronized (lock) { 141 } 142 }); 143 synchronized (lock) { 144 thread.start(); 145 ready.await(); 146 await(thread, Thread.State.BLOCKED); 147 } 148 thread.join(); 149 } 150 151 /** 152 * Test custom default scheduler execute method with bad parameters. 153 */ 154 @Test 155 void testExecuteThrows() throws Exception { 156 var ref = new AtomicReference<VirtualThreadScheduler>(); 157 Thread vthread = Thread.startVirtualThread(() -> { 158 ref.set(VirtualThreadScheduler.current()); 159 }); 160 vthread.join(); 161 VirtualThreadScheduler scheduler = ref.get(); 162 163 Runnable task = () -> { }; 164 165 // platform thread 166 Thread thread = Thread.ofPlatform().unstarted(() -> { }); 167 assertThrows(UnsupportedOperationException.class, () -> scheduler.execute(thread, task)); 168 169 // nulls 170 assertThrows(NullPointerException.class, () -> scheduler.execute(null, task)); 171 assertThrows(NullPointerException.class, () -> scheduler.execute(vthread, null)); 172 } 173 174 /** 175 * Test custom default scheduler delegating to builtin default scheduler. 176 */ 177 @Test 178 void testDelegatingToBuiltin() throws Exception { 179 assumeTrue(schedulerClassName.equals("CustomDefaultScheduler$CustomScheduler2")); 180 181 var ref = new AtomicReference<VirtualThreadScheduler>(); 182 Thread vthread = Thread.startVirtualThread(() -> { 183 ref.set(VirtualThreadScheduler.current()); 184 }); 185 vthread.join(); 186 187 var customScheduler1 = new CustomScheduler1(); 188 var customScheduler2 = (CustomScheduler2) ref.get(); 189 var builtinScheduler = customScheduler2.builtinScheduler(); 190 191 // ensure builtin default scheduler can't be mis-used 192 assertThrows(ClassCastException.class, () -> { var e = (Executor) builtinScheduler; }); 193 194 var vthread0 = Thread.ofVirtual().scheduler(builtinScheduler).unstarted(() -> { }); 195 var vthread1 = Thread.ofVirtual().scheduler(customScheduler1).unstarted(() -> { }); 196 var vthread2 = Thread.ofVirtual().scheduler(customScheduler2).unstarted(() -> { }); 197 198 Runnable task = () -> { }; 199 200 // builtin scheduler can execute tasks for itself or customScheduler2 201 builtinScheduler.execute(vthread0, task); 202 assertThrows(IllegalArgumentException.class, () -> builtinScheduler.execute(vthread1, task)); 203 builtinScheduler.execute(vthread2, task); 204 205 assertThrows(IllegalArgumentException.class, () -> customScheduler2.execute(vthread1, task)); 206 customScheduler2.execute(vthread2, task); 207 } 208 209 /** 210 * Waits for the given thread to reach a given state. 211 */ 212 private void await(Thread thread, Thread.State expectedState) throws InterruptedException { 213 Thread.State state = thread.getState(); 214 while (state != expectedState) { 215 assertTrue(state != Thread.State.TERMINATED, "Thread has terminated"); 216 Thread.sleep(10); 217 state = thread.getState(); 218 } 219 } 220 }