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 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 28 * @modules java.base/java.lang:+open 29 * @library /test/lib 30 * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorEnterExit 31 */ 32 33 /* 34 * @test id=LM_LEGACY 35 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 36 * @modules java.base/java.lang:+open 37 * @library /test/lib 38 * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit 39 */ 40 41 /* 42 * @test id=LM_LIGHTWEIGHT 43 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 44 * @modules java.base/java.lang:+open 45 * @library /test/lib 46 * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit 47 */ 48 49 /* 50 * @test id=Xint-LM_LEGACY 51 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 52 * @modules java.base/java.lang:+open 53 * @library /test/lib 54 * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit 55 */ 56 57 /* 58 * @test id=Xint-LM_LIGHTWEIGHT 59 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 60 * @modules java.base/java.lang:+open 61 * @library /test/lib 62 * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit 63 */ 64 65 /* 66 * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY 67 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 68 * @modules java.base/java.lang:+open 69 * @library /test/lib 70 * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit 71 */ 72 73 /* 74 * @test id=Xcomp-TieredStopAtLevel1-LM_LIGHTWEIGHT 75 * @modules java.base/java.lang:+open 76 * @library /test/lib 77 * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit 78 */ 79 80 /* 81 * @test id=Xcomp-noTieredCompilation-LM_LEGACY 82 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 83 * @modules java.base/java.lang:+open 84 * @library /test/lib 85 * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit 86 */ 87 88 /* 89 * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT 90 * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" 91 * @modules java.base/java.lang:+open 92 * @library /test/lib 93 * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit 94 */ 95 96 import java.time.Duration; 97 import java.util.ArrayList; 98 import java.util.List; 99 import java.util.concurrent.CountDownLatch; 100 import java.util.concurrent.ThreadFactory; 101 import java.util.concurrent.ThreadLocalRandom; 102 import java.util.concurrent.Executors; 103 import java.util.concurrent.ExecutorService; 104 import java.util.concurrent.atomic.AtomicBoolean; 105 import java.util.concurrent.locks.LockSupport; 106 import java.util.stream.IntStream; 107 import java.util.stream.Stream; 108 109 import jdk.test.lib.thread.VThreadRunner; 110 import jdk.test.lib.thread.VThreadPinner; 111 112 import org.junit.jupiter.api.Test; 113 import org.junit.jupiter.api.RepeatedTest; 114 import org.junit.jupiter.params.ParameterizedTest; 115 import org.junit.jupiter.params.provider.Arguments; 116 import org.junit.jupiter.params.provider.ValueSource; 117 import org.junit.jupiter.params.provider.MethodSource; 118 import org.junit.jupiter.api.condition.*; 119 import static org.junit.jupiter.api.Assertions.*; 120 import static org.junit.jupiter.api.Assumptions.*; 121 122 class MonitorEnterExit { 123 static final int MAX_VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors(); 124 static final int MAX_ENTER_DEPTH = 256; 125 126 /** 127 * Test monitor enter with no contention. 128 */ 129 @Test 130 void testEnterNoContention() throws Exception { 131 var lock = new Object(); 132 VThreadRunner.run(() -> { 133 synchronized (lock) { 134 assertTrue(Thread.holdsLock(lock)); 135 } 136 assertFalse(Thread.holdsLock(lock)); 137 }); 138 } 139 140 /** 141 * Test monitor enter with contention, monitor is held by platform thread. 142 */ 143 @Test 144 void testEnterWhenHeldByPlatformThread() throws Exception { 145 testEnterWithContention(); 146 } 147 148 /** 149 * Test monitor enter with contention, monitor is held by virtual thread. 150 */ 151 @Test 152 void testEnterWhenHeldByVirtualThread() throws Exception { 153 VThreadRunner.run(this::testEnterWithContention); 154 } 155 156 /** 157 * Test monitor enter with contention, monitor will be held by caller thread. 158 */ 159 private void testEnterWithContention() throws Exception { 160 var lock = new Object(); 161 var started = new CountDownLatch(1); 162 var entered = new AtomicBoolean(); 163 var vthread = Thread.ofVirtual().unstarted(() -> { 164 started.countDown(); 165 synchronized (lock) { 166 assertTrue(Thread.holdsLock(lock)); 167 entered.set(true); 168 } 169 assertFalse(Thread.holdsLock(lock)); 170 }); 171 try { 172 synchronized (lock) { 173 vthread.start(); 174 175 // wait for thread to start and block 176 started.await(); 177 await(vthread, Thread.State.BLOCKED); 178 179 assertFalse(entered.get()); 180 } 181 } finally { 182 vthread.join(); 183 } 184 assertTrue(entered.get()); 185 } 186 187 /** 188 * Test monitor reenter. 189 */ 190 @Test 191 void testReenter() throws Exception { 192 var lock = new Object(); 193 VThreadRunner.run(() -> { 194 testReenter(lock, 0); 195 assertFalse(Thread.holdsLock(lock)); 196 }); 197 } 198 199 private void testReenter(Object lock, int depth) { 200 if (depth < MAX_ENTER_DEPTH) { 201 synchronized (lock) { 202 assertTrue(Thread.holdsLock(lock)); 203 testReenter(lock, depth + 1); 204 assertTrue(Thread.holdsLock(lock)); 205 } 206 } 207 } 208 209 /** 210 * Test monitor reenter when there are other threads blocked trying to enter. 211 */ 212 @Test 213 void testReenterWithContention() throws Exception { 214 var lock = new Object(); 215 VThreadRunner.run(() -> { 216 List<Thread> threads = new ArrayList<>(); 217 testReenter(lock, 0, threads); 218 219 // wait for threads to terminate 220 for (Thread vthread : threads) { 221 vthread.join(); 222 } 223 }); 224 } 225 226 private void testReenter(Object lock, int depth, List<Thread> threads) throws Exception { 227 if (depth < MAX_ENTER_DEPTH) { 228 synchronized (lock) { 229 assertTrue(Thread.holdsLock(lock)); 230 231 // start platform or virtual thread that blocks waiting to enter 232 var started = new CountDownLatch(1); 233 ThreadFactory factory = ThreadLocalRandom.current().nextBoolean() 234 ? Thread.ofPlatform().factory() 235 : Thread.ofVirtual().factory(); 236 var thread = factory.newThread(() -> { 237 started.countDown(); 238 synchronized (lock) { 239 /* do nothing */ 240 } 241 }); 242 thread.start(); 243 244 // wait for thread to start and block 245 started.await(); 246 await(thread, Thread.State.BLOCKED); 247 threads.add(thread); 248 249 // test reenter 250 testReenter(lock, depth + 1, threads); 251 } 252 } 253 } 254 255 /** 256 * Test monitor enter when pinned. 257 */ 258 @Test 259 void testEnterWhenPinned() throws Exception { 260 var lock = new Object(); 261 VThreadPinner.runPinned(() -> { 262 synchronized (lock) { 263 assertTrue(Thread.holdsLock(lock)); 264 } 265 assertFalse(Thread.holdsLock(lock)); 266 }); 267 } 268 269 /** 270 * Test monitor reenter when pinned. 271 */ 272 @Test 273 void testReenterWhenPinned() throws Exception { 274 VThreadRunner.run(() -> { 275 var lock = new Object(); 276 synchronized (lock) { 277 VThreadPinner.runPinned(() -> { 278 assertTrue(Thread.holdsLock(lock)); 279 synchronized (lock) { 280 assertTrue(Thread.holdsLock(lock)); 281 } 282 assertTrue(Thread.holdsLock(lock)); 283 }); 284 } 285 assertFalse(Thread.holdsLock(lock)); 286 }); 287 } 288 289 /** 290 * Test contended monitor enter when pinned. Monitor is held by platform thread. 291 */ 292 @Test 293 void testContendedEnterWhenPinnedHeldByPlatformThread() throws Exception { 294 testEnterWithContentionWhenPinned(); 295 } 296 297 /** 298 * Test contended monitor enter when pinned. Monitor is held by virtual thread. 299 */ 300 @Test 301 void testContendedEnterWhenPinnedHeldByVirtualThread() throws Exception { 302 // need at least two carrier threads 303 int previousParallelism = VThreadRunner.ensureParallelism(2); 304 try { 305 VThreadRunner.run(this::testEnterWithContentionWhenPinned); 306 } finally { 307 VThreadRunner.setParallelism(previousParallelism); 308 } 309 } 310 311 /** 312 * Test contended monitor enter when pinned, monitor will be held by caller thread. 313 */ 314 private void testEnterWithContentionWhenPinned() throws Exception { 315 var lock = new Object(); 316 var started = new CountDownLatch(1); 317 var entered = new AtomicBoolean(); 318 Thread vthread = Thread.ofVirtual().unstarted(() -> { 319 VThreadPinner.runPinned(() -> { 320 started.countDown(); 321 synchronized (lock) { 322 entered.set(true); 323 } 324 }); 325 }); 326 synchronized (lock) { 327 // start thread and wait for it to block 328 vthread.start(); 329 started.await(); 330 await(vthread, Thread.State.BLOCKED); 331 assertFalse(entered.get()); 332 } 333 vthread.join(); 334 335 // check thread entered monitor 336 assertTrue(entered.get()); 337 } 338 339 /** 340 * Test that parking while holding a monitor releases the carrier. 341 */ 342 @ParameterizedTest 343 @ValueSource(booleans = { true, false }) 344 void testReleaseWhenParked(boolean reenter) throws Exception { 345 assumeTrue(ThreadBuilders.supportsCustomScheduler(), "No support for custom schedulers"); 346 try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { 347 Thread.Builder builder = ThreadBuilders.virtualThreadBuilder(scheduler); 348 349 var lock = new Object(); 350 351 // thread enters (and maybe reenters) a monitor and parks 352 var started = new CountDownLatch(1); 353 var vthread1 = builder.start(() -> { 354 started.countDown(); 355 synchronized (lock) { 356 if (reenter) { 357 synchronized (lock) { 358 LockSupport.park(); 359 } 360 } else { 361 LockSupport.park(); 362 } 363 } 364 }); 365 366 try { 367 // wait for thread to start and park 368 started.await(); 369 await(vthread1, Thread.State.WAITING); 370 371 // carrier should be released, use it for another thread 372 var executed = new AtomicBoolean(); 373 var vthread2 = builder.start(() -> { 374 executed.set(true); 375 }); 376 vthread2.join(); 377 assertTrue(executed.get()); 378 } finally { 379 LockSupport.unpark(vthread1); 380 vthread1.join(); 381 } 382 } 383 } 384 385 /** 386 * Test that blocking waiting to enter a monitor releases the carrier. 387 */ 388 @Test 389 void testReleaseWhenBlocked() throws Exception { 390 assumeTrue(ThreadBuilders.supportsCustomScheduler(), "No support for custom schedulers"); 391 try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { 392 Thread.Builder builder = ThreadBuilders.virtualThreadBuilder(scheduler); 393 394 var lock = new Object(); 395 396 // thread enters monitor 397 var started = new CountDownLatch(1); 398 var vthread1 = builder.unstarted(() -> { 399 started.countDown(); 400 synchronized (lock) { 401 } 402 }); 403 404 try { 405 synchronized (lock) { 406 // start thread and wait for it to block 407 vthread1.start(); 408 started.await(); 409 await(vthread1, Thread.State.BLOCKED); 410 411 // carrier should be released, use it for another thread 412 var executed = new AtomicBoolean(); 413 var vthread2 = builder.start(() -> { 414 executed.set(true); 415 }); 416 vthread2.join(); 417 assertTrue(executed.get()); 418 } 419 } finally { 420 vthread1.join(); 421 } 422 } 423 } 424 425 /** 426 * Test lots of virtual threads parked while holding a monitor. If the number of 427 * virtual threads exceeds the number of carrier threads then this test will hang if 428 * parking doesn't release the carrier. 429 */ 430 @Test 431 void testManyParkedThreads() throws Exception { 432 Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT]; 433 var done = new AtomicBoolean(); 434 for (int i = 0; i < MAX_VTHREAD_COUNT; i++) { 435 var lock = new Object(); 436 var started = new CountDownLatch(1); 437 var vthread = Thread.ofVirtual().start(() -> { 438 started.countDown(); 439 synchronized (lock) { 440 while (!done.get()) { 441 LockSupport.park(); 442 } 443 } 444 }); 445 // wait for thread to start and park 446 started.await(); 447 await(vthread, Thread.State.WAITING); 448 vthreads[i] = vthread; 449 } 450 451 // cleanup 452 done.set(true); 453 for (int i = 0; i < MAX_VTHREAD_COUNT; i++) { 454 var vthread = vthreads[i]; 455 LockSupport.unpark(vthread); 456 vthread.join(); 457 } 458 } 459 460 /** 461 * Test lots of virtual threads blocked waiting to enter a monitor. If the number 462 * of virtual threads exceeds the number of carrier threads this test will hang if 463 * carriers aren't released. 464 */ 465 @Test 466 void testManyBlockedThreads() throws Exception { 467 Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT]; 468 var lock = new Object(); 469 synchronized (lock) { 470 for (int i = 0; i < MAX_VTHREAD_COUNT; i++) { 471 var started = new CountDownLatch(1); 472 var vthread = Thread.ofVirtual().start(() -> { 473 started.countDown(); 474 synchronized (lock) { 475 } 476 }); 477 // wait for thread to start and block 478 started.await(); 479 await(vthread, Thread.State.BLOCKED); 480 vthreads[i] = vthread; 481 } 482 } 483 484 // cleanup 485 for (int i = 0; i < MAX_VTHREAD_COUNT; i++) { 486 vthreads[i].join(); 487 } 488 } 489 490 /** 491 * Returns a stream of elements that are ordered pairs of platform and virtual thread 492 * counts. 0,2,4,..16 platform threads. 2,4,6,..32 virtual threads. 493 */ 494 static Stream<Arguments> threadCounts() { 495 return IntStream.range(0, 17) 496 .filter(i -> i % 2 == 0) 497 .mapToObj(i -> i) 498 .flatMap(np -> IntStream.range(2, 33) 499 .filter(i -> i % 2 == 0) 500 .mapToObj(vp -> Arguments.of(np, vp))); 501 } 502 503 /** 504 * Test mutual exclusion of monitors with platform and virtual threads 505 */ 506 @ParameterizedTest 507 @MethodSource("threadCounts") 508 void testMutualExclusion(int nPlatformThreads, int nVirtualThreads) throws Exception { 509 class Counter { 510 int count; 511 synchronized void increment() { 512 count++; 513 Thread.yield(); 514 } 515 } 516 var counter = new Counter(); 517 int nThreads = nPlatformThreads + nVirtualThreads; 518 var threads = new Thread[nThreads]; 519 int index = 0; 520 for (int i = 0; i < nPlatformThreads; i++) { 521 threads[index] = Thread.ofPlatform() 522 .name("platform-" + index) 523 .unstarted(counter::increment); 524 index++; 525 } 526 for (int i = 0; i < nVirtualThreads; i++) { 527 threads[index] = Thread.ofVirtual() 528 .name("virtual-" + index) 529 .unstarted(counter::increment); 530 index++; 531 } 532 // start all threads 533 for (Thread thread : threads) { 534 thread.start(); 535 } 536 // wait for all threads to terminate 537 for (Thread thread : threads) { 538 thread.join(); 539 } 540 assertEquals(nThreads, counter.count); 541 } 542 543 /** 544 * Test unblocking a virtual thread waiting to enter a monitor held by a platform thread. 545 */ 546 @RepeatedTest(20) 547 void testUnblockingByPlatformThread() throws Exception { 548 testUnblocking(); 549 } 550 551 /** 552 * Test unblocking a virtual thread waiting to enter a monitor held by another 553 * virtual thread. 554 */ 555 @RepeatedTest(20) 556 void testUnblockingByVirtualThread() throws Exception { 557 VThreadRunner.run(this::testUnblocking); 558 } 559 560 /** 561 * Test unblocking a virtual thread waiting to enter a monitor, monitor will be 562 * initially be held by caller thread. 563 */ 564 private void testUnblocking() throws Exception { 565 var lock = new Object(); 566 var started = new CountDownLatch(1); 567 var entered = new AtomicBoolean(); 568 var vthread = Thread.ofVirtual().unstarted(() -> { 569 started.countDown(); 570 synchronized (lock) { 571 entered.set(true); 572 } 573 }); 574 try { 575 synchronized (lock) { 576 vthread.start(); 577 started.await(); 578 579 // random delay before exiting monitor 580 switch (ThreadLocalRandom.current().nextInt(4)) { 581 case 0 -> { /* no delay */} 582 case 1 -> Thread.onSpinWait(); 583 case 2 -> Thread.yield(); 584 case 3 -> await(vthread, Thread.State.BLOCKED); 585 default -> fail(); 586 } 587 588 assertFalse(entered.get()); 589 } 590 } finally { 591 vthread.join(); 592 } 593 assertTrue(entered.get()); 594 } 595 596 /** 597 * Test that unblocking a virtual thread waiting to enter a monitor does not consume 598 * the thread's parking permit. 599 */ 600 @Test 601 void testParkingPermitNotConsumed() throws Exception { 602 var lock = new Object(); 603 var started = new CountDownLatch(1); 604 var vthread = Thread.ofVirtual().unstarted(() -> { 605 started.countDown(); 606 LockSupport.unpark(Thread.currentThread()); 607 synchronized (lock) { } // should block 608 LockSupport.park(); // should not park 609 }); 610 611 synchronized (lock) { 612 vthread.start(); 613 // wait for thread to start and block 614 started.await(); 615 await(vthread, Thread.State.BLOCKED); 616 } 617 vthread.join(); 618 } 619 620 /** 621 * Test that unblocking a virtual thread waiting to enter a monitor does not make 622 * available the thread's parking permit. 623 */ 624 @Test 625 void testParkingPermitNotOffered() throws Exception { 626 var lock = new Object(); 627 var started = new CountDownLatch(1); 628 var vthread = Thread.ofVirtual().unstarted(() -> { 629 started.countDown(); 630 synchronized (lock) { } // should block 631 LockSupport.park(); // should park 632 }); 633 634 synchronized (lock) { 635 vthread.start(); 636 // wait for thread to start and block 637 started.await(); 638 await(vthread, Thread.State.BLOCKED); 639 } 640 641 try { 642 // wait for thread to park, it should not terminate 643 await(vthread, Thread.State.WAITING); 644 vthread.join(Duration.ofMillis(100)); 645 assertEquals(Thread.State.WAITING, vthread.getState()); 646 } finally { 647 LockSupport.unpark(vthread); 648 vthread.join(); 649 } 650 } 651 652 /** 653 * Waits for the given thread to reach a given state. 654 */ 655 private void await(Thread thread, Thread.State expectedState) throws InterruptedException { 656 Thread.State state = thread.getState(); 657 while (state != expectedState) { 658 assertTrue(state != Thread.State.TERMINATED, "Thread has terminated"); 659 Thread.sleep(10); 660 state = thread.getState(); 661 } 662 } 663 }