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