1 /* 2 * Copyright (c) 2023, 2024, 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 id=default 26 * @summary Test virtual thread with monitor enter/exit 27 * @modules java.base/java.lang:+open 28 * @library /test/lib 29 * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorEnterExit 30 */ 31 32 import java.time.Duration; 33 import java.util.ArrayList; 34 import java.util.List; 35 import java.util.concurrent.CountDownLatch; 36 import java.util.concurrent.ThreadFactory; 37 import java.util.concurrent.ThreadLocalRandom; 38 import java.util.concurrent.Executors; 39 import java.util.concurrent.ExecutorService; 40 import java.util.concurrent.atomic.AtomicBoolean; 41 import java.util.concurrent.locks.LockSupport; 42 import java.util.stream.IntStream; 43 import java.util.stream.Stream; 44 45 import jdk.test.lib.thread.VThreadPinner; 46 import jdk.test.lib.thread.VThreadRunner; 47 import jdk.test.lib.thread.VThreadScheduler; 48 import org.junit.jupiter.api.Test; 49 import org.junit.jupiter.api.BeforeAll; 50 import org.junit.jupiter.api.RepeatedTest; 51 import org.junit.jupiter.params.ParameterizedTest; 52 import org.junit.jupiter.params.provider.Arguments; 53 import org.junit.jupiter.params.provider.ValueSource; 54 import org.junit.jupiter.params.provider.MethodSource; 55 import org.junit.jupiter.api.condition.*; 56 import static org.junit.jupiter.api.Assertions.*; 57 import static org.junit.jupiter.api.Assumptions.*; 58 59 class MonitorEnterExit { 60 static final int MAX_ENTER_DEPTH = 256; 61 62 @BeforeAll 63 static void setup() { 64 // need >=2 carriers for testing pinning when main thread is a virtual thread 65 if (Thread.currentThread().isVirtual()) { 66 VThreadRunner.ensureParallelism(2); 67 } 68 } 69 70 /** 71 * Test monitor enter with no contention. 72 */ 73 @Test 74 void testEnterNoContention() throws Exception { 75 var lock = new Object(); 76 VThreadRunner.run(() -> { 77 synchronized (lock) { 78 assertTrue(Thread.holdsLock(lock)); 79 } 80 assertFalse(Thread.holdsLock(lock)); 81 }); 82 } 83 84 /** 85 * Test monitor enter with contention, monitor is held by platform thread. 86 */ 87 @Test 88 void testEnterWhenHeldByPlatformThread() throws Exception { 89 testEnterWithContention(); 90 } 91 92 /** 93 * Test monitor enter with contention, monitor is held by virtual thread. 94 */ 95 @Test 96 void testEnterWhenHeldByVirtualThread() throws Exception { 97 VThreadRunner.run(this::testEnterWithContention); 98 } 99 100 /** 101 * Test monitor enter with contention, monitor will be held by caller thread. 102 */ 103 private void testEnterWithContention() throws Exception { 104 var lock = new Object(); 105 var started = new CountDownLatch(1); 106 var entered = new AtomicBoolean(); 107 var vthread = Thread.ofVirtual().unstarted(() -> { 108 started.countDown(); 109 synchronized (lock) { 110 assertTrue(Thread.holdsLock(lock)); 111 entered.set(true); 112 } 113 assertFalse(Thread.holdsLock(lock)); 114 }); 115 try { 116 synchronized (lock) { 117 vthread.start(); 118 119 // wait for thread to start and block 120 started.await(); 121 await(vthread, Thread.State.BLOCKED); 122 123 assertFalse(entered.get()); 124 } 125 } finally { 126 vthread.join(); 127 } 128 assertTrue(entered.get()); 129 } 130 131 /** 132 * Test monitor reenter. 133 */ 134 @Test 135 void testReenter() throws Exception { 136 var lock = new Object(); 137 VThreadRunner.run(() -> { 138 testReenter(lock, 0); 139 assertFalse(Thread.holdsLock(lock)); 140 }); 141 } 142 143 private void testReenter(Object lock, int depth) { 144 if (depth < MAX_ENTER_DEPTH) { 145 synchronized (lock) { 146 assertTrue(Thread.holdsLock(lock)); 147 testReenter(lock, depth + 1); 148 assertTrue(Thread.holdsLock(lock)); 149 } 150 } 151 } 152 153 /** 154 * Test monitor enter when pinned. 155 */ 156 @Test 157 void testEnterWhenPinned() throws Exception { 158 var lock = new Object(); 159 VThreadPinner.runPinned(() -> { 160 synchronized (lock) { 161 assertTrue(Thread.holdsLock(lock)); 162 } 163 assertFalse(Thread.holdsLock(lock)); 164 }); 165 } 166 167 /** 168 * Test monitor reenter when pinned. 169 */ 170 @Test 171 void testReenterWhenPinned() throws Exception { 172 VThreadRunner.run(() -> { 173 var lock = new Object(); 174 synchronized (lock) { 175 VThreadPinner.runPinned(() -> { 176 assertTrue(Thread.holdsLock(lock)); 177 synchronized (lock) { 178 assertTrue(Thread.holdsLock(lock)); 179 } 180 assertTrue(Thread.holdsLock(lock)); 181 }); 182 } 183 assertFalse(Thread.holdsLock(lock)); 184 }); 185 } 186 187 /** 188 * Test contended monitor enter when pinned. Monitor is held by platform thread. 189 */ 190 @Test 191 void testContendedEnterWhenPinnedHeldByPlatformThread() throws Exception { 192 testEnterWithContentionWhenPinned(); 193 } 194 195 /** 196 * Test contended monitor enter when pinned. Monitor is held by virtual thread. 197 */ 198 @Test 199 void testContendedEnterWhenPinnedHeldByVirtualThread() throws Exception { 200 // need at least two carrier threads 201 int previousParallelism = VThreadRunner.ensureParallelism(2); 202 try { 203 VThreadRunner.run(this::testEnterWithContentionWhenPinned); 204 } finally { 205 VThreadRunner.setParallelism(previousParallelism); 206 } 207 } 208 209 /** 210 * Test contended monitor enter when pinned, monitor will be held by caller thread. 211 */ 212 private void testEnterWithContentionWhenPinned() throws Exception { 213 var lock = new Object(); 214 var started = new CountDownLatch(1); 215 var entered = new AtomicBoolean(); 216 Thread vthread = Thread.ofVirtual().unstarted(() -> { 217 VThreadPinner.runPinned(() -> { 218 started.countDown(); 219 synchronized (lock) { 220 entered.set(true); 221 } 222 }); 223 }); 224 synchronized (lock) { 225 // start thread and wait for it to block 226 vthread.start(); 227 started.await(); 228 await(vthread, Thread.State.BLOCKED); 229 assertFalse(entered.get()); 230 } 231 vthread.join(); 232 233 // check thread entered monitor 234 assertTrue(entered.get()); 235 } 236 237 /** 238 * Returns a stream of elements that are ordered pairs of platform and virtual thread 239 * counts. 0,2,4,..16 platform threads. 2,4,6,..32 virtual threads. 240 */ 241 static Stream<Arguments> threadCounts() { 242 return IntStream.range(0, 17) 243 .filter(i -> i % 2 == 0) 244 .mapToObj(i -> i) 245 .flatMap(np -> IntStream.range(2, 33) 246 .filter(i -> i % 2 == 0) 247 .mapToObj(vp -> Arguments.of(np, vp))); 248 } 249 250 /** 251 * Test mutual exclusion of monitors with platform and virtual threads. 252 */ 253 @ParameterizedTest 254 @MethodSource("threadCounts") 255 void testMutualExclusion(int nPlatformThreads, int nVirtualThreads) throws Exception { 256 class Counter { 257 int count; 258 synchronized void increment() { 259 count++; 260 Thread.yield(); 261 } 262 } 263 var counter = new Counter(); 264 int nThreads = nPlatformThreads + nVirtualThreads; 265 var threads = new Thread[nThreads]; 266 int index = 0; 267 for (int i = 0; i < nPlatformThreads; i++) { 268 threads[index] = Thread.ofPlatform() 269 .name("platform-" + index) 270 .unstarted(counter::increment); 271 index++; 272 } 273 for (int i = 0; i < nVirtualThreads; i++) { 274 threads[index] = Thread.ofVirtual() 275 .name("virtual-" + index) 276 .unstarted(counter::increment); 277 index++; 278 } 279 // start all threads 280 for (Thread thread : threads) { 281 thread.start(); 282 } 283 // wait for all threads to terminate 284 for (Thread thread : threads) { 285 thread.join(); 286 } 287 assertEquals(nThreads, counter.count); 288 } 289 290 /** 291 * Test unblocking a virtual thread waiting to enter a monitor held by a platform thread. 292 */ 293 @RepeatedTest(20) 294 void testUnblockingByPlatformThread() throws Exception { 295 testUnblocking(); 296 } 297 298 /** 299 * Test unblocking a virtual thread waiting to enter a monitor held by another 300 * virtual thread. 301 */ 302 @RepeatedTest(20) 303 void testUnblockingByVirtualThread() throws Exception { 304 VThreadRunner.run(this::testUnblocking); 305 } 306 307 /** 308 * Test unblocking a virtual thread waiting to enter a monitor, monitor will be 309 * initially be held by caller thread. 310 */ 311 private void testUnblocking() throws Exception { 312 var lock = new Object(); 313 var started = new CountDownLatch(1); 314 var entered = new AtomicBoolean(); 315 var vthread = Thread.ofVirtual().unstarted(() -> { 316 started.countDown(); 317 synchronized (lock) { 318 entered.set(true); 319 } 320 }); 321 try { 322 synchronized (lock) { 323 vthread.start(); 324 started.await(); 325 326 // random delay before exiting monitor 327 switch (ThreadLocalRandom.current().nextInt(4)) { 328 case 0 -> { /* no delay */} 329 case 1 -> Thread.onSpinWait(); 330 case 2 -> Thread.yield(); 331 case 3 -> await(vthread, Thread.State.BLOCKED); 332 default -> fail(); 333 } 334 335 assertFalse(entered.get()); 336 } 337 } finally { 338 vthread.join(); 339 } 340 assertTrue(entered.get()); 341 } 342 343 /** 344 * Test that unblocking a virtual thread waiting to enter a monitor does not consume 345 * the thread's parking permit. 346 */ 347 @Test 348 void testParkingPermitNotConsumed() throws Exception { 349 var lock = new Object(); 350 var started = new CountDownLatch(1); 351 var vthread = Thread.ofVirtual().unstarted(() -> { 352 started.countDown(); 353 LockSupport.unpark(Thread.currentThread()); 354 synchronized (lock) { } // should block 355 LockSupport.park(); // should not park 356 }); 357 358 synchronized (lock) { 359 vthread.start(); 360 // wait for thread to start and block 361 started.await(); 362 await(vthread, Thread.State.BLOCKED); 363 } 364 vthread.join(); 365 } 366 367 /** 368 * Test that unblocking a virtual thread waiting to enter a monitor does not make 369 * available the thread's parking permit. 370 */ 371 @Test 372 void testParkingPermitNotOffered() throws Exception { 373 var lock = new Object(); 374 var started = new CountDownLatch(1); 375 var vthread = Thread.ofVirtual().unstarted(() -> { 376 started.countDown(); 377 synchronized (lock) { } // should block 378 LockSupport.park(); // should park 379 }); 380 381 synchronized (lock) { 382 vthread.start(); 383 // wait for thread to start and block 384 started.await(); 385 await(vthread, Thread.State.BLOCKED); 386 } 387 388 try { 389 // wait for thread to park, it should not terminate 390 await(vthread, Thread.State.WAITING); 391 vthread.join(Duration.ofMillis(100)); 392 assertEquals(Thread.State.WAITING, vthread.getState()); 393 } finally { 394 LockSupport.unpark(vthread); 395 vthread.join(); 396 } 397 } 398 399 /** 400 * Waits for the given thread to reach a given state. 401 */ 402 private void await(Thread thread, Thread.State expectedState) throws InterruptedException { 403 Thread.State state = thread.getState(); 404 while (state != expectedState) { 405 assertTrue(state != Thread.State.TERMINATED, "Thread has terminated"); 406 Thread.sleep(10); 407 state = thread.getState(); 408 } 409 } 410 }