1 /*
  2  * Copyright (c) 2019, 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 threads using Object.wait/notifyAll
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 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 MonitorWaitNotify
 94  */
 95 
 96 import java.util.ArrayList;
 97 import java.util.List;
 98 import java.util.Set;
 99 import java.util.concurrent.CountDownLatch;
100 import java.util.concurrent.Executors;
101 import java.util.concurrent.ExecutorService;
102 import java.util.concurrent.ThreadFactory;
103 import java.util.concurrent.TimeUnit;
104 import java.util.concurrent.atomic.AtomicBoolean;
105 import java.util.concurrent.atomic.AtomicInteger;
106 import java.util.concurrent.atomic.AtomicReference;
107 import java.util.concurrent.locks.LockSupport;
108 import java.util.stream.IntStream;
109 import java.util.stream.Stream;
110 import java.util.stream.Collectors;
111 
112 import jdk.test.lib.thread.VThreadScheduler;
113 import jdk.test.lib.thread.VThreadRunner;
114 import jdk.test.lib.thread.VThreadRunner;
115 import jdk.test.lib.thread.VThreadPinner;
116 import org.junit.jupiter.api.Test;
117 import org.junit.jupiter.api.BeforeAll;
118 import org.junit.jupiter.params.provider.Arguments;
119 import org.junit.jupiter.params.ParameterizedTest;
120 import org.junit.jupiter.params.provider.MethodSource;
121 import org.junit.jupiter.params.provider.ValueSource;
122 import static org.junit.jupiter.api.Assertions.*;
123 import static org.junit.jupiter.api.Assumptions.*;
124 
125 class MonitorWaitNotify {
126 
127     @BeforeAll
128     static void setup() {
129         // need >=2 carriers for testing pinning
130         VThreadRunner.ensureParallelism(2);
131     }
132 
133     /**
134      * Test virtual thread waits, notified by platform thread.
135      */
136     @ParameterizedTest
137     @ValueSource(booleans = { true, false })
138     void testWaitNotify1(boolean pinned) throws Exception {
139         var lock = new Object();
140         var ready = new AtomicBoolean();
141         var thread = Thread.ofVirtual().start(() -> {
142             synchronized (lock) {
143                 try {
144                     if (pinned) {
145                         VThreadPinner.runPinned(() -> {
146                             ready.set(true);
147                             lock.wait();
148                         });
149                     } else {
150                         ready.set(true);
151                         lock.wait();
152                     }
153                 } catch (InterruptedException e) { }
154             }
155         });
156         awaitTrue(ready);
157 
158         // notify, thread should block waiting to reenter
159         synchronized (lock) {
160             lock.notifyAll();
161             await(thread, Thread.State.BLOCKED);
162         }
163         thread.join();
164     }
165 
166     /**
167      * Test platform thread waits, notified by virtual thread.
168      */
169     @Test
170     void testWaitNotify2() throws Exception {
171         var lock = new Object();
172         var thread = Thread.ofVirtual().unstarted(() -> {
173             synchronized (lock) {
174                 lock.notifyAll();
175             }
176         });
177         synchronized (lock) {
178             thread.start();
179             lock.wait();
180         }
181         thread.join();
182     }
183 
184     /**
185      * Test virtual thread waits, notified by another virtual thread.
186      */
187     @ParameterizedTest
188     @ValueSource(booleans = { true, false })
189     void testWaitNotify3(boolean pinned) throws Exception {
190         var lock = new Object();
191         var ready = new AtomicBoolean();
192         var thread1 = Thread.ofVirtual().start(() -> {
193             synchronized (lock) {
194                 try {
195                     if (pinned) {
196                         VThreadPinner.runPinned(() -> {
197                             ready.set(true);
198                             lock.wait();
199                         });
200                     } else {
201                         ready.set(true);
202                         lock.wait();
203                     }
204                 } catch (InterruptedException e) {
205                     e.printStackTrace();
206                 }
207             }
208         });
209         var thread2 = Thread.ofVirtual().start(() -> {
210             try {
211                 awaitTrue(ready);
212 
213                 // notify, thread should block waiting to reenter
214                 synchronized (lock) {
215                     lock.notifyAll();
216                     await(thread1, Thread.State.BLOCKED);
217                 }
218             } catch (InterruptedException e) {
219                 e.printStackTrace();
220             }
221         });
222         thread1.join();
223         thread2.join();
224     }
225 
226     /**
227      * Test notifyAll when there are no threads waiting.
228      */
229     @ParameterizedTest
230     @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
231     void testNotifyBeforeWait(int timeout) throws Exception {
232         var lock = new Object();
233 
234         // no threads waiting
235         synchronized (lock) {
236             lock.notifyAll();
237         }
238 
239         var ready = new AtomicBoolean();
240         var thread = Thread.ofVirtual().start(() -> {
241             try {
242                 synchronized (lock) {
243                     ready.set(true);
244 
245                     // thread should wait
246                     if (timeout > 0) {
247                         lock.wait(timeout);
248                     } else {
249                         lock.wait();
250                     }
251                 }
252             } catch (InterruptedException e) { }
253         });
254 
255         try {
256             // wait for thread to start and wait
257             awaitTrue(ready);
258             Thread.State expectedState = timeout > 0
259                     ? Thread.State.TIMED_WAITING
260                     : Thread.State.WAITING;
261             await(thread, expectedState);
262 
263             // poll thread state again, it should still be waiting
264             Thread.sleep(10);
265             assertEquals(thread.getState(), expectedState);
266         } finally {
267             synchronized (lock) {
268                 lock.notifyAll();
269             }
270             thread.join();
271         }
272     }
273 
274     /**
275      * Returns a stream of elements that are ordered pairs of platform and virtual thread
276      * counts. 0,2,4,..8 platform threads. 2,4,6,..16 virtual threads.
277      */
278     static Stream<Arguments> threadCounts() {
279         return IntStream.range(0, 9)
280                 .filter(i -> i % 2 == 0)
281                 .mapToObj(i -> i)
282                 .flatMap(np -> IntStream.range(2, 17)
283                         .filter(i -> i % 2 == 0)
284                         .mapToObj(vp -> Arguments.of(np, vp)));
285     }
286 
287     /**
288      * Test notify wakes only one thread when platform and virtual threads are waiting.
289      */
290     @ParameterizedTest
291     @MethodSource("threadCounts")
292     void testNotifyOneThread(int nPlatformThreads, int nVirtualThreads) throws Exception {
293         int nThreads = nPlatformThreads + nVirtualThreads;
294 
295         var lock = new Object();
296         var ready = new CountDownLatch(nThreads);
297         var notified = new AtomicInteger();
298 
299         Runnable waitTask = () -> {
300             synchronized (lock) {
301                 try {
302                     ready.countDown();
303                     lock.wait();
304                     notified.incrementAndGet();
305                 } catch (InterruptedException e) { }
306             }
307         };
308 
309         var threads = new ArrayList<Thread>();
310         try {
311             for (int i = 0; i < nPlatformThreads; i++) {
312                 threads.add(Thread.ofPlatform().start(waitTask));
313             }
314             for (int i = 0; i < nVirtualThreads; i++) {
315                 threads.add(Thread.ofVirtual().start(waitTask));
316             }
317 
318             // wait for all threads to wait
319             ready.await();
320 
321             // wake threads, one by one
322             for (int i = 0; i < threads.size(); i++) {
323 
324                 // wake one thread
325                 synchronized (lock) {
326                     lock.notify();
327                 }
328 
329                 // one thread should have awoken
330                 int expectedWakeups = i + 1;
331                 while (notified.get() < expectedWakeups) {
332                     Thread.sleep(10);
333                 }
334                 assertEquals(expectedWakeups, notified.get());
335             }
336         } finally {
337             for (Thread t : threads) {
338                 t.interrupt();
339                 t.join();
340             }
341         }
342     }
343 
344     /**
345      * Test notifyAll wakes all threads.
346      */
347     @ParameterizedTest
348     @MethodSource("threadCounts")
349     void testNotifyAllThreads(int nPlatformThreads, int nVirtualThreads) throws Exception {
350         int nThreads = nPlatformThreads + nVirtualThreads;
351 
352         var lock = new Object();
353         var ready = new CountDownLatch(nThreads);
354         var notified = new CountDownLatch(nThreads);
355 
356         Runnable waitTask = () -> {
357             synchronized (lock) {
358                 try {
359                     ready.countDown();
360                     lock.wait();
361                     notified.countDown();
362                 } catch (InterruptedException e) { }
363             }
364         };
365 
366         var threads = new ArrayList<Thread>();
367         try {
368             for (int i = 0; i < nPlatformThreads; i++) {
369                 threads.add(Thread.ofPlatform().start(waitTask));
370             }
371             for (int i = 0; i < nVirtualThreads; i++) {
372                 threads.add(Thread.ofVirtual().start(waitTask));
373             }
374 
375             // wait for all threads to wait
376             ready.await();
377 
378             // wakeup all threads
379             synchronized (lock) {
380                 lock.notifyAll();
381             }
382 
383             // wait for all threads to have awoken
384             notified.await();
385 
386         } finally {
387             for (Thread t : threads) {
388                 t.interrupt();
389                 t.join();
390             }
391         }
392     }
393 
394     /**
395      * Test duration of timed Object.wait.
396      */
397     @Test
398     void testTimedWaitDuration1() throws Exception {
399         var lock = new Object();
400 
401         var durationRef = new AtomicReference<Long>();
402         var thread = Thread.ofVirtual().start(() -> {
403             try {
404                 synchronized (lock) {
405                     long start = millisTime();
406                     lock.wait(2000);
407                     durationRef.set(millisTime() - start);
408                 }
409             } catch (InterruptedException e) { }
410         });
411 
412         thread.join();
413 
414         long duration = durationRef.get();
415         checkDuration(duration, 1900, 20_000);
416     }
417 
418     /**
419      * Test duration of timed Object.wait. This test invokes wait twice, first with a short
420      * timeout, the second with a longer timeout. The test scenario ensures that the
421      * timeout from the first wait doesn't interfere with the second wait.
422      */
423     @Test
424     void testTimedWaitDuration2() throws Exception {
425         var lock = new Object();
426 
427         var ready = new AtomicBoolean();
428         var waited = new AtomicBoolean();
429         var durationRef = new AtomicReference<Long>();
430         var thread = Thread.ofVirtual().start(() -> {
431             try {
432                 synchronized (lock) {
433                     ready.set(true);
434                     lock.wait(200);
435                     waited.set(true);
436 
437                     long start = millisTime();
438                     lock.wait(2000);
439                     durationRef.set(millisTime() - start);
440                 }
441             } catch (InterruptedException e) { }
442         });
443 
444         awaitTrue(ready);
445         synchronized (lock) {
446             // wake thread if waiting in first wait
447             if (!waited.get()) {
448                 lock.notifyAll();
449             }
450         }
451 
452         thread.join();
453 
454         long duration = durationRef.get();
455         checkDuration(duration, 1900, 20_000);
456     }
457 
458     /**
459      * Testing invoking Object.wait with interrupt status set.
460      */
461     @ParameterizedTest
462     @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
463     void testWaitWithInterruptSet(int timeout) throws Exception {
464         VThreadRunner.run(() -> {
465             Object lock = new Object();
466             synchronized (lock) {
467                 Thread.currentThread().interrupt();
468                 if (timeout > 0) {
469                     assertThrows(InterruptedException.class, () -> lock.wait(timeout));
470                 } else {
471                     assertThrows(InterruptedException.class, lock::wait);
472                 }
473                 assertFalse(Thread.currentThread().isInterrupted());
474             }
475         });
476     }
477 
478     /**
479      * Test interrupting a virtual thread waiting in Object.wait.
480      */
481     @ParameterizedTest
482     @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
483     void testInterruptWait(int timeout) throws Exception {
484         var lock = new Object();
485         var ready = new AtomicBoolean();
486         var interruptedException = new AtomicBoolean();
487         var vthread = Thread.ofVirtual().start(() -> {
488             synchronized (lock) {
489                 try {
490                     ready.set(true);
491                     if (timeout > 0) {
492                         lock.wait(timeout);
493                     } else {
494                         lock.wait();
495                     }
496                 } catch (InterruptedException e) {
497                     // check stack trace has the expected frames
498                     Set<String> expected = Set.of("wait0", "wait", "run");
499                     Set<String> methods = Stream.of(e.getStackTrace())
500                             .map(StackTraceElement::getMethodName)
501                             .collect(Collectors.toSet());
502                     assertTrue(methods.containsAll(expected));
503 
504                     interruptedException.set(true);
505                 }
506             }
507         });
508 
509         // wait for thread to start and wait
510         awaitTrue(ready);
511         await(vthread, timeout > 0 ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
512 
513         // interrupt thread, should block, then throw InterruptedException
514         synchronized (lock) {
515             vthread.interrupt();
516             await(vthread, Thread.State.BLOCKED);
517         }
518         vthread.join();
519         assertTrue(interruptedException.get());
520     }
521 
522     /**
523      * Test interrupting a virtual thread blocked waiting to reenter after waiting.
524      */
525     @ParameterizedTest
526     @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
527     void testInterruptReenterAfterWait(int timeout) throws Exception {
528         var lock = new Object();
529         var ready = new AtomicBoolean();
530         var interruptedException = new AtomicBoolean();
531         var vthread = Thread.ofVirtual().start(() -> {
532             synchronized (lock) {
533                 try {
534                     ready.set(true);
535                     if (timeout > 0) {
536                         lock.wait(timeout);
537                     } else {
538                         lock.wait();
539                     }
540                 } catch (InterruptedException e) {
541                     interruptedException.set(true);
542                 }
543             }
544         });
545 
546         // wait for thread to start and wait
547         awaitTrue(ready);
548         await(vthread, timeout > 0 ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
549 
550         // notify, thread should block waiting to reenter
551         synchronized (lock) {
552             lock.notifyAll();
553             await(vthread, Thread.State.BLOCKED);
554 
555             // interrupt when blocked
556             vthread.interrupt();
557         }
558 
559         vthread.join();
560         assertFalse(interruptedException.get());
561         assertTrue(vthread.isInterrupted());
562     }
563 
564     /**
565      * Test Object.wait when the monitor entry count > 1.
566      */
567     @ParameterizedTest
568     @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
569     void testWaitWhenEnteredManyTimes(int timeout) throws Exception {
570         var lock = new Object();
571         var ready = new AtomicBoolean();
572         var vthread = Thread.ofVirtual().start(() -> {
573             synchronized (lock) {
574                 synchronized (lock) {
575                     synchronized (lock) {
576                         try {
577                             ready.set(true);
578                             if (timeout > 0) {
579                                 lock.wait(timeout);
580                             } else {
581                                 lock.wait();
582                             }
583                         } catch (InterruptedException e) { }
584                     }
585                 }
586             }
587         });
588 
589         // wait for thread to start and wait
590         awaitTrue(ready);
591         await(vthread, timeout > 0 ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
592 
593         // notify, thread should block waiting to reenter
594         synchronized (lock) {
595             lock.notifyAll();
596             await(vthread, Thread.State.BLOCKED);
597         }
598         vthread.join();
599     }
600 
601     /**
602      * Test that Object.wait does not consume the thread's parking permit.
603      */
604     @Test
605     void testParkingPermitNotConsumed() throws Exception {
606         var lock = new Object();
607         var started = new CountDownLatch(1);
608         var completed = new AtomicBoolean();
609         var vthread = Thread.ofVirtual().start(() -> {
610             started.countDown();
611             LockSupport.unpark(Thread.currentThread());
612             synchronized (lock) {
613                 try {
614                     lock.wait();
615                 } catch (InterruptedException e) {
616                     fail("wait interrupted");
617                 }
618             }
619             LockSupport.park();      // should not park
620             completed.set(true);
621         });
622 
623         // wait for thread to start and wait
624         started.await();
625         await(vthread, Thread.State.WAITING);
626 
627         // wakeup thread
628         synchronized (lock) {
629             lock.notifyAll();
630         }
631 
632         // thread should terminate
633         vthread.join();
634         assertTrue(completed.get());
635     }
636 
637     /**
638      * Test that Object.wait does not make available the thread's parking permit.
639      */
640     @Test
641     void testParkingPermitNotOffered() throws Exception {
642         var lock = new Object();
643         var started = new CountDownLatch(1);
644         var readyToPark = new CountDownLatch(1);
645         var completed = new AtomicBoolean();
646         var vthread = Thread.ofVirtual().start(() -> {
647             started.countDown();
648             synchronized (lock) {
649                 try {
650                     lock.wait();
651                 } catch (InterruptedException e) {
652                     fail("wait interrupted");
653                 }
654             }
655             readyToPark.countDown();
656             LockSupport.park();      // should park
657             completed.set(true);
658         });
659 
660         // wait for thread to start and wait
661         started.await();
662         await(vthread, Thread.State.WAITING);
663 
664         // wakeup thread
665         synchronized (lock) {
666             lock.notifyAll();
667         }
668 
669         // thread should park
670         readyToPark.await();
671         await(vthread, Thread.State.WAITING);
672 
673         LockSupport.unpark(vthread);
674 
675         // thread should terminate
676         vthread.join();
677         assertTrue(completed.get());
678     }
679 
680     /**
681      * Test that Object.wait releases the carrier. This test uses a custom scheduler
682      * with one carrier thread.
683      */
684     @ParameterizedTest
685     @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
686     void testReleaseWhenWaiting1(int timeout) throws Exception {
687         assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
688         try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
689             ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
690 
691             var lock = new Object();
692             var ready = new AtomicBoolean();
693             var completed = new AtomicBoolean();
694 
695             var vthread1 = factory.newThread(() -> {
696                 synchronized (lock) {
697                     try {
698                         ready.set(true);
699                         if (timeout > 0) {
700                             lock.wait(timeout);
701                         } else {
702                             lock.wait();
703                         }
704                     } catch (InterruptedException e) {
705                         fail("wait interrupted");
706                     }
707                 }
708                 completed.set(true);
709             });
710             vthread1.start();
711 
712             // wait for vthread1 to start and wait
713             awaitTrue(ready);
714             await(vthread1, timeout > 0 ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
715 
716             // carrier should be released, use it for another thread
717             var executed = new AtomicBoolean();
718             var vthread2 = factory.newThread(() -> {
719                 executed.set(true);
720             });
721             vthread2.start();
722             vthread2.join();
723             assertTrue(executed.get());
724 
725             // wakeup vthread1
726             synchronized (lock) {
727                 lock.notifyAll();
728             }
729 
730             vthread1.join();
731             assertTrue(completed.get());
732         }
733     }
734 
735     /**
736      * Test that Object.wait releases the carrier. This test arranges for 4*ncores - 1
737      * virtual threads to wait. For long timeout and no timeout cases, all virtual threads
738      * will wait until they are notified.
739      */
740     @ParameterizedTest
741     @ValueSource(ints = { 0, 10, 20, 100, 500, 30000, Integer.MAX_VALUE })
742     void testReleaseWhenWaiting2(int timeout) throws Exception {
743         int VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors();
744         CountDownLatch latch = new CountDownLatch(VTHREAD_COUNT);
745         Object lock = new Object();
746         AtomicInteger counter = new AtomicInteger(0);
747 
748         for (int i = 0; i < VTHREAD_COUNT; i++) {
749             Thread.ofVirtual().name("vthread-" + i).start(() -> {
750                 synchronized (lock) {
751                     if (counter.incrementAndGet() == VTHREAD_COUNT) {
752                         lock.notifyAll();
753                     } else {
754                         try {
755                             if (timeout > 0) {
756                                 lock.wait(timeout);
757                             } else {
758                                 lock.wait();
759                             }
760                         } catch (InterruptedException e) {}
761                     }
762                 }
763                 latch.countDown();
764             });
765         }
766         latch.await();
767     }
768 
769     /**
770      * Test that wait(long) throws IAE when timeout is negative.
771      */
772     @Test
773     void testIllegalArgumentException() throws Exception {
774         VThreadRunner.run(() -> {
775             Object obj = new Object();
776             synchronized (obj) {
777                 assertThrows(IllegalArgumentException.class, () -> obj.wait(-1L));
778                 assertThrows(IllegalArgumentException.class, () -> obj.wait(-1000L));
779                 assertThrows(IllegalArgumentException.class, () -> obj.wait(Long.MIN_VALUE));
780             }
781         });
782     }
783 
784     /**
785      * Test that wait throws IMSE when not owner.
786      */
787     @Test
788     void testIllegalMonitorStateException() throws Exception {
789         VThreadRunner.run(() -> {
790             Object obj = new Object();
791             assertThrows(IllegalMonitorStateException.class, () -> obj.wait());
792             assertThrows(IllegalMonitorStateException.class, () -> obj.wait(0));
793             assertThrows(IllegalMonitorStateException.class, () -> obj.wait(1000));
794             assertThrows(IllegalMonitorStateException.class, () -> obj.wait(Long.MAX_VALUE));
795         });
796     }
797 
798     /**
799      * Waits for the boolean value to become true.
800      */
801     private static void awaitTrue(AtomicBoolean ref) throws InterruptedException {
802         while (!ref.get()) {
803             Thread.sleep(20);
804         }
805     }
806 
807     /**
808      * Waits for the given thread to reach a given state.
809      */
810     private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
811         Thread.State state = thread.getState();
812         while (state != expectedState) {
813             assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
814             Thread.sleep(10);
815             state = thread.getState();
816         }
817     }
818 
819     /**
820      * Returns the current time in milliseconds.
821      */
822     private static long millisTime() {
823         long now = System.nanoTime();
824         return TimeUnit.MILLISECONDS.convert(now, TimeUnit.NANOSECONDS);
825     }
826 
827     /**
828      * Check a duration is within expected bounds.
829      * @param duration, in milliseconds
830      * @param min minimum expected duration, in milliseconds
831      * @param max maximum expected duration, in milliseconds
832      * @return the duration (now - start), in milliseconds
833      */
834     private static void checkDuration(long duration, long min, long max) {
835         assertTrue(duration >= min,
836                 "Duration " + duration + "ms, expected >= " + min + "ms");
837         assertTrue(duration <= max,
838                 "Duration " + duration + "ms, expected <= " + max + "ms");
839     }
840 }