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 }