| /* | |
| * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test id=default | |
| * @bug 8284161 8286788 8321270 | |
| * @summary Test Thread API with virtual threads | |
| * @modules java.base/java.lang:+open jdk.management | |
| * @library /test/lib | |
| * @build LockingMode | |
| * @run junit/othervm/native --enable-native-access=ALL-UNNAMED ThreadAPI | |
| */ | |
| /* | |
| * @test id=no-vmcontinuations | |
| * @requires vm.continuations | |
| * @modules java.base/java.lang:+open jdk.management | |
| * @library /test/lib | |
| * @build LockingMode | |
| * @run junit/othervm/native -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations | |
| * --enable-native-access=ALL-UNNAMED ThreadAPI | |
| */ | |
| import java.time.Duration; | |
| import java.util.Arrays; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.concurrent.CopyOnWriteArrayList; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executor; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.ForkJoinPool; | |
| import java.util.concurrent.ScheduledExecutorService; | |
| import java.util.concurrent.ThreadFactory; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.concurrent.locks.LockSupport; | |
| import java.util.concurrent.locks.ReentrantLock; | |
| import java.util.stream.Stream; | |
| import java.nio.channels.Selector; | |
| import jdk.test.lib.thread.VThreadPinner; | |
| import jdk.test.lib.thread.VThreadRunner; // ensureParallelism requires jdk.management | |
| import jdk.test.lib.thread.VThreadScheduler; | |
| import org.junit.jupiter.api.Test; | |
| import org.junit.jupiter.api.BeforeAll; | |
| import org.junit.jupiter.api.AfterAll; | |
| import org.junit.jupiter.api.Disabled; | |
| import org.junit.jupiter.api.condition.DisabledIf; | |
| import org.junit.jupiter.params.ParameterizedTest; | |
| import org.junit.jupiter.params.provider.MethodSource; | |
| import org.junit.jupiter.params.provider.ValueSource; | |
| import static org.junit.jupiter.api.Assertions.*; | |
| import static org.junit.jupiter.api.Assumptions.*; | |
| class ThreadAPI { | |
| private static final Object lock = new Object(); | |
| // used for scheduling thread interrupt | |
| private static ScheduledExecutorService scheduler; | |
| @BeforeAll | |
| static void setup() { | |
| ThreadFactory factory = Executors.defaultThreadFactory(); | |
| scheduler = Executors.newSingleThreadScheduledExecutor(factory); | |
| // need >=2 carriers for testing pinning | |
| VThreadRunner.ensureParallelism(2); | |
| } | |
| @AfterAll | |
| static void finish() { | |
| scheduler.shutdown(); | |
| } | |
| /** | |
| * An operation that does not return a result but may throw an exception. | |
| */ | |
| @FunctionalInterface | |
| interface ThrowingRunnable { | |
| void run() throws Exception; | |
| } | |
| /** | |
| * Test Thread.currentThread before/after park. | |
| */ | |
| @Test | |
| void testCurrentThread1() throws Exception { | |
| var before = new AtomicReference<Thread>(); | |
| var after = new AtomicReference<Thread>(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| before.set(Thread.currentThread()); | |
| LockSupport.park(); | |
| after.set(Thread.currentThread()); | |
| }); | |
| await(thread, Thread.State.WAITING); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| assertTrue(before.get() == thread); | |
| assertTrue(after.get() == thread); | |
| } | |
| /** | |
| * Test Thread.currentThread before/after entering synchronized block. | |
| */ | |
| @Test | |
| void testCurrentThread2() throws Exception { | |
| var ref1 = new AtomicReference<Thread>(); | |
| var ref2 = new AtomicReference<Thread>(); | |
| var ref3 = new AtomicReference<Thread>(); | |
| var thread = Thread.ofVirtual().unstarted(() -> { | |
| ref1.set(Thread.currentThread()); | |
| synchronized (lock) { | |
| ref2.set(Thread.currentThread()); | |
| } | |
| ref3.set(Thread.currentThread()); | |
| }); | |
| synchronized (lock) { | |
| thread.start(); | |
| await(thread, Thread.State.BLOCKED); | |
| } | |
| thread.join(); | |
| assertTrue(ref1.get() == thread); | |
| assertTrue(ref2.get() == thread); | |
| assertTrue(ref3.get() == thread); | |
| } | |
| /** | |
| * Test Thread.currentThread before/after acquiring lock. | |
| */ | |
| @Test | |
| void testCurrentThread3() throws Exception { | |
| var ref1 = new AtomicReference<Thread>(); | |
| var ref2 = new AtomicReference<Thread>(); | |
| var ref3 = new AtomicReference<Thread>(); | |
| var lock = new ReentrantLock(); | |
| var thread = Thread.ofVirtual().unstarted(() -> { | |
| ref1.set(Thread.currentThread()); | |
| lock.lock(); | |
| try { | |
| ref2.set(Thread.currentThread()); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| ref3.set(Thread.currentThread()); | |
| }); | |
| lock.lock(); | |
| try { | |
| thread.start(); | |
| await(thread, Thread.State.WAITING); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| thread.join(); | |
| assertTrue(ref1.get() == thread); | |
| assertTrue(ref2.get() == thread); | |
| assertTrue(ref3.get() == thread); | |
| } | |
| /** | |
| * Test Thread::run. | |
| */ | |
| @Test | |
| void testRun1() throws Exception { | |
| var ref = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().unstarted(() -> ref.set(true)); | |
| thread.run(); | |
| assertFalse(ref.get()); | |
| } | |
| /** | |
| * Test Thread::start. | |
| */ | |
| @Test | |
| void testStart1() throws Exception { | |
| var ref = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().unstarted(() -> ref.set(true)); | |
| assertFalse(ref.get()); | |
| thread.start(); | |
| thread.join(); | |
| assertTrue(ref.get()); | |
| } | |
| /** | |
| * Test Thread::start, thread already started. | |
| */ | |
| @Test | |
| void testStart2() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| try { | |
| assertThrows(IllegalThreadStateException.class, thread::start); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::start, thread already terminated. | |
| */ | |
| @Test | |
| void testStart3() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { }); | |
| thread.join(); | |
| assertThrows(IllegalThreadStateException.class, thread::start); | |
| } | |
| /** | |
| * Test Thread.startVirtualThread. | |
| */ | |
| @Test | |
| void testStartVirtualThread() throws Exception { | |
| var ref = new AtomicReference<Thread>(); | |
| Thread vthread = Thread.startVirtualThread(() -> { | |
| ref.set(Thread.currentThread()); | |
| LockSupport.park(); | |
| }); | |
| try { | |
| assertTrue(vthread.isVirtual()); | |
| // Thread.currentThread() returned by the virtual thread | |
| Thread current; | |
| while ((current = ref.get()) == null) { | |
| Thread.sleep(10); | |
| } | |
| assertTrue(current == vthread); | |
| } finally { | |
| LockSupport.unpark(vthread); | |
| } | |
| assertThrows(NullPointerException.class, () -> Thread.startVirtualThread(null)); | |
| } | |
| /** | |
| * Test Thread::stop from current thread. | |
| */ | |
| @Test | |
| void testStop1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread t = Thread.currentThread(); | |
| assertThrows(UnsupportedOperationException.class, t::stop); | |
| }); | |
| } | |
| /** | |
| * Test Thread::stop from another thread. | |
| */ | |
| @Test | |
| void testStop2() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| Thread.sleep(20*1000); | |
| } catch (InterruptedException e) { } | |
| }); | |
| try { | |
| assertThrows(UnsupportedOperationException.class, thread::stop); | |
| } finally { | |
| thread.interrupt(); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread.join before thread starts, platform thread invokes join. | |
| */ | |
| @Test | |
| void testJoin1() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(() -> { }); | |
| thread.join(); | |
| thread.join(0); | |
| thread.join(0, 0); | |
| thread.join(100); | |
| thread.join(100, 0); | |
| assertThrows(IllegalThreadStateException.class, | |
| () -> thread.join(Duration.ofMillis(-100))); | |
| assertThrows(IllegalThreadStateException.class, | |
| () -> thread.join(Duration.ofMillis(0))); | |
| assertThrows(IllegalThreadStateException.class, | |
| () -> thread.join(Duration.ofMillis(100))); | |
| } | |
| /** | |
| * Test Thread.join before thread starts, virtual thread invokes join. | |
| */ | |
| @Test | |
| void testJoin2() throws Exception { | |
| VThreadRunner.run(this::testJoin1); | |
| } | |
| /** | |
| * Test Thread.join where thread does not terminate, platform thread invokes join. | |
| */ | |
| @Test | |
| void testJoin3() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| try { | |
| thread.join(20); | |
| thread.join(20, 0); | |
| thread.join(20, 20); | |
| thread.join(0, 20); | |
| assertFalse(thread.join(Duration.ofMillis(-20))); | |
| assertFalse(thread.join(Duration.ofMillis(0))); | |
| assertFalse(thread.join(Duration.ofMillis(20))); | |
| assertTrue(thread.isAlive()); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread.join where thread does not terminate, virtual thread invokes join. | |
| */ | |
| @Test | |
| void testJoin4() throws Exception { | |
| VThreadRunner.run(this::testJoin3); | |
| } | |
| /** | |
| * Test Thread.join where thread terminates, platform thread invokes join. | |
| */ | |
| @Test | |
| void testJoin5() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| Thread.sleep(50); | |
| } catch (InterruptedException e) { } | |
| }); | |
| thread.join(); | |
| assertFalse(thread.isAlive()); | |
| } | |
| /** | |
| * Test Thread.join where thread terminates, virtual thread invokes join. | |
| */ | |
| @Test | |
| void testJoin6() throws Exception { | |
| VThreadRunner.run(this::testJoin5); | |
| } | |
| /** | |
| * Test Thread.join where thread terminates, platform thread invokes timed-join. | |
| */ | |
| @Test | |
| void testJoin7() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| Thread.sleep(50); | |
| } catch (InterruptedException e) { } | |
| }); | |
| thread.join(10*1000); | |
| assertFalse(thread.isAlive()); | |
| } | |
| /** | |
| * Test Thread.join where thread terminates, virtual thread invokes timed-join. | |
| */ | |
| @Test | |
| void testJoin8() throws Exception { | |
| VThreadRunner.run(this::testJoin7); | |
| } | |
| /** | |
| * Test Thread.join where thread terminates, platform thread invokes timed-join. | |
| */ | |
| @Test | |
| void testJoin11() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| Thread.sleep(50); | |
| } catch (InterruptedException e) { } | |
| }); | |
| assertTrue(thread.join(Duration.ofSeconds(10))); | |
| assertFalse(thread.isAlive()); | |
| } | |
| /** | |
| * Test Thread.join where thread terminates, virtual thread invokes timed-join. | |
| */ | |
| @Test | |
| void testJoin12() throws Exception { | |
| VThreadRunner.run(this::testJoin11); | |
| } | |
| /** | |
| * Test Thread.join where thread already terminated, platform thread invokes join. | |
| */ | |
| @Test | |
| void testJoin13() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { }); | |
| while (thread.isAlive()) { | |
| Thread.sleep(10); | |
| } | |
| thread.join(); | |
| thread.join(0); | |
| thread.join(0, 0); | |
| thread.join(100); | |
| thread.join(100, 0); | |
| assertTrue(thread.join(Duration.ofMillis(-100))); | |
| assertTrue(thread.join(Duration.ofMillis(0))); | |
| assertTrue(thread.join(Duration.ofMillis(100))); | |
| } | |
| /** | |
| * Test Thread.join where thread already terminated, virtual thread invokes join. | |
| */ | |
| @Test | |
| void testJoin14() throws Exception { | |
| VThreadRunner.run(this::testJoin13); | |
| } | |
| /** | |
| * Test platform thread invoking Thread.join with interrupt status set. | |
| */ | |
| @Test | |
| void testJoin15() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| Thread.currentThread().interrupt(); | |
| try { | |
| assertThrows(InterruptedException.class, thread::join); | |
| } finally { | |
| Thread.interrupted(); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test virtual thread invoking Thread.join with interrupt status set. | |
| */ | |
| @Test | |
| void testJoin16() throws Exception { | |
| VThreadRunner.run(this::testJoin15); | |
| } | |
| /** | |
| * Test platform thread invoking timed-Thread.join with interrupt status set. | |
| */ | |
| @Test | |
| void testJoin17() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| Thread.currentThread().interrupt(); | |
| try { | |
| assertThrows(InterruptedException.class, () -> thread.join(100)); | |
| } finally { | |
| Thread.interrupted(); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test virtual thread invoking timed-Thread.join with interrupt status set. | |
| */ | |
| @Test | |
| void testJoin18() throws Exception { | |
| VThreadRunner.run(this::testJoin17); | |
| } | |
| /** | |
| * Test platform thread invoking timed-Thread.join with interrupt status set. | |
| */ | |
| @Test | |
| void testJoin19() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| Thread.currentThread().interrupt(); | |
| try { | |
| assertThrows(InterruptedException.class, | |
| () -> thread.join(Duration.ofMillis(100))); | |
| } finally { | |
| Thread.interrupted(); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test virtual thread invoking timed-Thread.join with interrupt status set. | |
| */ | |
| @Test | |
| void testJoin20() throws Exception { | |
| VThreadRunner.run(this::testJoin19); | |
| } | |
| /** | |
| * Test interrupt of platform thread blocked in Thread.join. | |
| */ | |
| @Test | |
| void testJoin21() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| scheduleInterrupt(Thread.currentThread(), 100); | |
| try { | |
| assertThrows(InterruptedException.class, thread::join); | |
| } finally { | |
| Thread.interrupted(); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test interrupt of virtual thread blocked in Thread.join. | |
| */ | |
| @Test | |
| void testJoin22() throws Exception { | |
| VThreadRunner.run(this::testJoin17); | |
| } | |
| /** | |
| * Test interrupt of platform thread blocked in timed-Thread.join. | |
| */ | |
| @Test | |
| void testJoin23() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| scheduleInterrupt(Thread.currentThread(), 100); | |
| try { | |
| assertThrows(InterruptedException.class, () -> thread.join(10*1000)); | |
| } finally { | |
| Thread.interrupted(); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test interrupt of virtual thread blocked in Thread.join. | |
| */ | |
| @Test | |
| void testJoin24() throws Exception { | |
| VThreadRunner.run(this::testJoin23); | |
| } | |
| /** | |
| * Test interrupt of platform thread blocked in Thread.join. | |
| */ | |
| @Test | |
| void testJoin25() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| scheduleInterrupt(Thread.currentThread(), 100); | |
| try { | |
| assertThrows(InterruptedException.class, | |
| () -> thread.join(Duration.ofSeconds(10))); | |
| } finally { | |
| Thread.interrupted(); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test interrupt of virtual thread blocked in Thread.join. | |
| */ | |
| @Test | |
| void testJoin26() throws Exception { | |
| VThreadRunner.run(this::testJoin25); | |
| } | |
| /** | |
| * Test virtual thread calling Thread.join to wait for platform thread to terminate. | |
| */ | |
| @Test | |
| void testJoin27() throws Exception { | |
| AtomicBoolean done = new AtomicBoolean(); | |
| VThreadRunner.run(() -> { | |
| var thread = new Thread(() -> { | |
| while (!done.get()) { | |
| LockSupport.park(); | |
| } | |
| }); | |
| thread.start(); | |
| try { | |
| assertFalse(thread.join(Duration.ofMillis(-100))); | |
| assertFalse(thread.join(Duration.ofMillis(0))); | |
| assertFalse(thread.join(Duration.ofMillis(100))); | |
| } finally { | |
| done.set(true); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test virtual thread calling Thread.join to wait for platform thread to terminate. | |
| */ | |
| @Test | |
| void testJoin28() throws Exception { | |
| long nanos = TimeUnit.NANOSECONDS.convert(100, TimeUnit.MILLISECONDS); | |
| VThreadRunner.run(() -> { | |
| var thread = new Thread(() -> LockSupport.parkNanos(nanos)); | |
| thread.start(); | |
| try { | |
| assertTrue(thread.join(Duration.ofSeconds(Integer.MAX_VALUE))); | |
| assertFalse(thread.isAlive()); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test virtual thread with interrupt status set calling Thread.join to wait | |
| * for platform thread to terminate. | |
| */ | |
| @Test | |
| void testJoin29() throws Exception { | |
| VThreadRunner.run(() -> { | |
| var thread = new Thread(LockSupport::park); | |
| thread.start(); | |
| Thread.currentThread().interrupt(); | |
| try { | |
| thread.join(Duration.ofSeconds(Integer.MAX_VALUE)); | |
| fail("join not interrupted"); | |
| } catch (InterruptedException expected) { | |
| assertFalse(Thread.interrupted()); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test interrupting virtual thread that is waiting in Thread.join for | |
| * platform thread to terminate. | |
| */ | |
| @Test | |
| void testJoin30() throws Exception { | |
| VThreadRunner.run(() -> { | |
| AtomicBoolean done = new AtomicBoolean(); | |
| var thread = new Thread(() -> { | |
| while (!done.get()) { | |
| LockSupport.park(); | |
| } | |
| }); | |
| thread.start(); | |
| scheduleInterrupt(Thread.currentThread(), 100); | |
| try { | |
| thread.join(Duration.ofSeconds(Integer.MAX_VALUE)); | |
| fail("join not interrupted"); | |
| } catch (InterruptedException expected) { | |
| assertFalse(Thread.interrupted()); | |
| } finally { | |
| done.set(true); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test platform thread invoking Thread.join on a thread that is parking | |
| * and unparking. | |
| */ | |
| @Test | |
| void testJoin31() throws Exception { | |
| Thread thread = Thread.ofVirtual().start(() -> { | |
| synchronized (lock) { | |
| for (int i = 0; i < 10; i++) { | |
| LockSupport.parkNanos(Duration.ofMillis(20).toNanos()); | |
| } | |
| } | |
| }); | |
| thread.join(); | |
| assertFalse(thread.isAlive()); | |
| } | |
| /** | |
| * Test virtual thread invoking Thread.join on a thread that is parking | |
| * and unparking. | |
| */ | |
| @Test | |
| void testJoin32() throws Exception { | |
| VThreadRunner.run(this::testJoin31); | |
| } | |
| /** | |
| * Test platform thread invoking timed-Thread.join on a thread that is parking | |
| * and unparking while pinned. | |
| */ | |
| @Test | |
| void testJoin33() throws Exception { | |
| AtomicBoolean done = new AtomicBoolean(); | |
| Thread thread = Thread.ofVirtual().start(() -> { | |
| VThreadPinner.runPinned(() -> { | |
| while (!done.get()) { | |
| LockSupport.parkNanos(Duration.ofMillis(20).toNanos()); | |
| } | |
| }); | |
| }); | |
| try { | |
| assertFalse(thread.join(Duration.ofMillis(100))); | |
| } finally { | |
| done.set(true); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test virtual thread invoking timed-Thread.join on a thread that is parking | |
| * and unparking while pinned. | |
| */ | |
| @Test | |
| void testJoin34() throws Exception { | |
| VThreadRunner.run(this::testJoin33); | |
| } | |
| /** | |
| * Test Thread.join(null). | |
| */ | |
| @Test | |
| void testJoin35() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(LockSupport::park); | |
| // unstarted | |
| assertThrows(NullPointerException.class, () -> thread.join(null)); | |
| // started | |
| thread.start(); | |
| try { | |
| assertThrows(NullPointerException.class, () -> thread.join(null)); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| } | |
| thread.join(); | |
| // terminated | |
| assertThrows(NullPointerException.class, () -> thread.join(null)); | |
| } | |
| /** | |
| * Test Thread.interrupt on current thread. | |
| */ | |
| @Test | |
| void testInterrupt1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| assertFalse(me.isInterrupted()); | |
| me.interrupt(); | |
| assertTrue(me.isInterrupted()); | |
| Thread.interrupted(); // clear interrupt status | |
| assertFalse(me.isInterrupted()); | |
| me.interrupt(); | |
| }); | |
| } | |
| /** | |
| * Test Thread.interrupt before thread started. | |
| */ | |
| @Test | |
| void testInterrupt2() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(() -> { }); | |
| thread.interrupt(); | |
| assertTrue(thread.isInterrupted()); | |
| } | |
| /** | |
| * Test Thread.interrupt after thread started. | |
| */ | |
| @Test | |
| void testInterrupt3() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { }); | |
| thread.join(); | |
| thread.interrupt(); | |
| assertTrue(thread.isInterrupted()); | |
| } | |
| /** | |
| * Test termination with interrupt status set. | |
| */ | |
| @Test | |
| void testInterrupt4() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| Thread.currentThread().interrupt(); | |
| }); | |
| thread.join(); | |
| assertTrue(thread.isInterrupted()); | |
| } | |
| /** | |
| * Test Thread.interrupt of thread blocked in Selector.select. | |
| */ | |
| @Test | |
| void testInterrupt5() throws Exception { | |
| var exception = new AtomicReference<Exception>(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| try (var sel = Selector.open()) { | |
| sel.select(); | |
| assertTrue(Thread.currentThread().isInterrupted()); | |
| } | |
| } catch (Exception e) { | |
| exception.set(e); | |
| } | |
| }); | |
| Thread.sleep(100); // give time for thread to block | |
| thread.interrupt(); | |
| thread.join(); | |
| assertNull(exception.get()); | |
| } | |
| /** | |
| * Test Thread.interrupt of thread parked in sleep. | |
| */ | |
| @Test | |
| void testInterrupt6() throws Exception { | |
| var exception = new AtomicReference<Exception>(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| try { | |
| Thread.sleep(60*1000); | |
| fail("sleep not interrupted"); | |
| } catch (InterruptedException e) { | |
| // interrupt status should be reset | |
| assertFalse(Thread.interrupted()); | |
| } | |
| } catch (Exception e) { | |
| exception.set(e); | |
| } | |
| }); | |
| await(thread, Thread.State.TIMED_WAITING); | |
| thread.interrupt(); | |
| thread.join(); | |
| assertNull(exception.get()); | |
| } | |
| /** | |
| * Test Thread.interrupt of parked thread. | |
| */ | |
| @Test | |
| void testInterrupt7() throws Exception { | |
| var exception = new AtomicReference<Exception>(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| LockSupport.park(); | |
| assertTrue(Thread.currentThread().isInterrupted()); | |
| } catch (Exception e) { | |
| exception.set(e); | |
| } | |
| }); | |
| await(thread, Thread.State.WAITING); | |
| thread.interrupt(); | |
| thread.join(); | |
| assertNull(exception.get()); | |
| } | |
| /** | |
| * Test trying to park with interrupt status set. | |
| */ | |
| @Test | |
| void testInterrupt8() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.interrupt(); | |
| LockSupport.park(); | |
| assertTrue(Thread.interrupted()); | |
| }); | |
| } | |
| /** | |
| * Test trying to wait with interrupt status set. | |
| */ | |
| @Test | |
| void testInterrupt9() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.interrupt(); | |
| synchronized (lock) { | |
| try { | |
| lock.wait(); | |
| fail("wait not interrupted"); | |
| } catch (InterruptedException expected) { | |
| assertFalse(Thread.interrupted()); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Test trying to block with interrupt status set. | |
| */ | |
| @Test | |
| void testInterrupt10() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.interrupt(); | |
| try (Selector sel = Selector.open()) { | |
| sel.select(); | |
| assertTrue(Thread.interrupted()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test Thread.getName and setName from current thread, started without name. | |
| */ | |
| @Test | |
| void testSetName1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| assertTrue(me.getName().isEmpty()); | |
| me.setName("fred"); | |
| assertEquals("fred", me.getName()); | |
| }); | |
| } | |
| /** | |
| * Test Thread.getName and setName from current thread, started with name. | |
| */ | |
| @Test | |
| void testSetName2() throws Exception { | |
| VThreadRunner.run("fred", () -> { | |
| Thread me = Thread.currentThread(); | |
| assertEquals("fred", me.getName()); | |
| me.setName("joe"); | |
| assertEquals("joe", me.getName()); | |
| }); | |
| } | |
| /** | |
| * Test Thread.getName and setName from another thread. | |
| */ | |
| @Test | |
| void testSetName3() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(LockSupport::park); | |
| assertTrue(thread.getName().isEmpty()); | |
| // not started | |
| thread.setName("fred1"); | |
| assertEquals("fred1", thread.getName()); | |
| // started | |
| thread.start(); | |
| try { | |
| assertEquals("fred1", thread.getName()); | |
| thread.setName("fred2"); | |
| assertEquals("fred2", thread.getName()); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| // terminated | |
| assertEquals("fred2", thread.getName()); | |
| thread.setName("fred3"); | |
| assertEquals("fred3", thread.getName()); | |
| } | |
| /** | |
| * Test Thread.getPriority and setPriority from current thread. | |
| */ | |
| @Test | |
| void testSetPriority1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| assertEquals(Thread.NORM_PRIORITY, me.getPriority()); | |
| me.setPriority(Thread.MAX_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, me.getPriority()); | |
| me.setPriority(Thread.NORM_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, me.getPriority()); | |
| me.setPriority(Thread.MIN_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, me.getPriority()); | |
| assertThrows(IllegalArgumentException.class, () -> me.setPriority(-1)); | |
| }); | |
| } | |
| /** | |
| * Test Thread.getPriority and setPriority from another thread. | |
| */ | |
| @Test | |
| void testSetPriority2() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(LockSupport::park); | |
| // not started | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| thread.setPriority(Thread.MAX_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| thread.setPriority(Thread.NORM_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| thread.setPriority(Thread.MIN_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| assertThrows(IllegalArgumentException.class, () -> thread.setPriority(-1)); | |
| // running | |
| thread.start(); | |
| try { | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| thread.setPriority(Thread.NORM_PRIORITY); | |
| thread.setPriority(Thread.MAX_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| thread.setPriority(Thread.NORM_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| thread.setPriority(Thread.MIN_PRIORITY); | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| assertThrows(IllegalArgumentException.class, () -> thread.setPriority(-1)); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| } | |
| thread.join(); | |
| // terminated | |
| assertEquals(Thread.NORM_PRIORITY, thread.getPriority()); | |
| } | |
| /** | |
| * Test Thread.isDaemon and setDaemon from current thread. | |
| */ | |
| @Test | |
| void testSetDaemon1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| assertTrue(me.isDaemon()); | |
| assertThrows(IllegalThreadStateException.class, () -> me.setDaemon(true)); | |
| assertThrows(IllegalArgumentException.class, () -> me.setDaemon(false)); | |
| }); | |
| } | |
| /** | |
| * Test Thread.isDaemon and setDaemon from another thread. | |
| */ | |
| @Test | |
| void testSetDaemon2() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(LockSupport::park); | |
| // not started | |
| assertTrue(thread.isDaemon()); | |
| thread.setDaemon(true); | |
| assertThrows(IllegalArgumentException.class, () -> thread.setDaemon(false)); | |
| // running | |
| thread.start(); | |
| try { | |
| assertTrue(thread.isDaemon()); | |
| assertThrows(IllegalThreadStateException.class, () -> thread.setDaemon(true)); | |
| assertThrows(IllegalArgumentException.class, () -> thread.setDaemon(false)); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| } | |
| thread.join(); | |
| // terminated | |
| assertTrue(thread.isDaemon()); | |
| } | |
| /** | |
| * Test Thread.yield releases carrier thread. | |
| */ | |
| @Test | |
| void testYieldReleasesCarrier() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| var list = new CopyOnWriteArrayList<String>(); | |
| try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| var thread = factory.newThread(() -> { | |
| list.add("A"); | |
| var child = factory.newThread(() -> { | |
| list.add("B"); | |
| Thread.yield(); | |
| list.add("B"); | |
| }); | |
| child.start(); | |
| Thread.yield(); | |
| list.add("A"); | |
| try { child.join(); } catch (InterruptedException e) { } | |
| }); | |
| thread.start(); | |
| thread.join(); | |
| } | |
| assertEquals(List.of("A", "B", "A", "B"), list); | |
| } | |
| /** | |
| * Test Thread.yield releases carrier thread when virtual thread holds a monitor. | |
| */ | |
| @Test | |
| @DisabledIf("LockingMode#isLegacy") | |
| void testYieldReleasesCarrierWhenHoldingMonitor() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| var list = new CopyOnWriteArrayList<String>(); | |
| try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| var lock = new Object(); | |
| var thread = factory.newThread(() -> { | |
| list.add("A"); | |
| var child = factory.newThread(() -> { | |
| list.add("B"); | |
| synchronized (lock) { | |
| Thread.yield(); | |
| } | |
| list.add("B"); | |
| }); | |
| child.start(); | |
| Thread.yield(); | |
| list.add("A"); | |
| try { child.join(); } catch (InterruptedException e) { } | |
| }); | |
| thread.start(); | |
| thread.join(); | |
| } | |
| assertEquals(List.of("A", "B", "A", "B"), list); | |
| } | |
| /** | |
| * Test Thread.yield when thread is pinned. | |
| */ | |
| @Test | |
| void testYieldWhenPinned() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| var list = new CopyOnWriteArrayList<String>(); | |
| try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| var thread = factory.newThread(() -> { | |
| list.add("A"); | |
| var child = factory.newThread(() -> { | |
| list.add("B"); | |
| }); | |
| child.start(); | |
| VThreadPinner.runPinned(() -> { | |
| Thread.yield(); // pinned so will be a no-op | |
| list.add("A"); | |
| }); | |
| try { child.join(); } catch (InterruptedException e) { } | |
| }); | |
| thread.start(); | |
| thread.join(); | |
| } | |
| assertEquals(List.of("A", "A", "B"), list); | |
| } | |
| /** | |
| * Test Thread.yield does not consume the thread's parking permit. | |
| */ | |
| @Test | |
| void testYieldDoesNotConsumParkingPermit() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| LockSupport.unpark(Thread.currentThread()); | |
| Thread.yield(); | |
| LockSupport.park(); // should not park | |
| }); | |
| thread.join(); | |
| } | |
| /** | |
| * Test Thread.yield does not make available the thread's parking permit. | |
| */ | |
| @Test | |
| void testYieldDoesNotOfferParkingPermit() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { | |
| Thread.yield(); | |
| LockSupport.park(); // should park | |
| }); | |
| try { | |
| await(thread, Thread.State.WAITING); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread.onSpinWait. | |
| */ | |
| @Test | |
| void testOnSpinWait() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| Thread.onSpinWait(); | |
| assertTrue(Thread.currentThread() == me); | |
| }); | |
| } | |
| /** | |
| * Test Thread.sleep(-1). | |
| */ | |
| @Test | |
| void testSleep1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| assertThrows(IllegalArgumentException.class, () -> Thread.sleep(-1)); | |
| assertThrows(IllegalArgumentException.class, () -> Thread.sleep(-1, 0)); | |
| assertThrows(IllegalArgumentException.class, () -> Thread.sleep(0, -1)); | |
| assertThrows(IllegalArgumentException.class, () -> Thread.sleep(0, 1_000_000)); | |
| }); | |
| VThreadRunner.run(() -> Thread.sleep(Duration.ofMillis(-1))); | |
| } | |
| /** | |
| * Test Thread.sleep(0). | |
| */ | |
| @Test | |
| void testSleep2() throws Exception { | |
| VThreadRunner.run(() -> Thread.sleep(0)); | |
| VThreadRunner.run(() -> Thread.sleep(0, 0)); | |
| VThreadRunner.run(() -> Thread.sleep(Duration.ofMillis(0))); | |
| } | |
| /** | |
| * Tasks that sleep for 1 second. | |
| */ | |
| static Stream<ThrowingRunnable> oneSecondSleepers() { | |
| return Stream.of( | |
| () -> Thread.sleep(1000), | |
| () -> Thread.sleep(Duration.ofSeconds(1)) | |
| ); | |
| } | |
| /** | |
| * Test Thread.sleep duration. | |
| */ | |
| @ParameterizedTest | |
| @MethodSource("oneSecondSleepers") | |
| void testSleep3(ThrowingRunnable sleeper) throws Exception { | |
| VThreadRunner.run(() -> { | |
| long start = millisTime(); | |
| sleeper.run(); | |
| expectDuration(start, /*min*/900, /*max*/20_000); | |
| }); | |
| } | |
| /** | |
| * Tasks that sleep for zero or longer duration. | |
| */ | |
| static Stream<ThrowingRunnable> sleepers() { | |
| return Stream.of( | |
| () -> Thread.sleep(0), | |
| () -> Thread.sleep(0, 0), | |
| () -> Thread.sleep(1000), | |
| () -> Thread.sleep(1000, 0), | |
| () -> Thread.sleep(Duration.ofMillis(0)), | |
| () -> Thread.sleep(Duration.ofMillis(1000)) | |
| ); | |
| } | |
| /** | |
| * Test Thread.sleep with interrupt status set. | |
| */ | |
| @ParameterizedTest | |
| @MethodSource("sleepers") | |
| void testSleep4(ThrowingRunnable sleeper) throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.interrupt(); | |
| try { | |
| sleeper.run(); | |
| fail("sleep was not interrupted"); | |
| } catch (InterruptedException e) { | |
| // expected | |
| assertFalse(me.isInterrupted()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test Thread.sleep with interrupt status set and a negative duration. | |
| */ | |
| @Test | |
| void testSleep4() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.interrupt(); | |
| Thread.sleep(Duration.ofMillis(-1000)); // does nothing | |
| assertTrue(me.isInterrupted()); | |
| }); | |
| } | |
| /** | |
| * Tasks that sleep for a long time. | |
| */ | |
| static Stream<ThrowingRunnable> longSleepers() { | |
| return Stream.of( | |
| () -> Thread.sleep(20_000), | |
| () -> Thread.sleep(20_000, 0), | |
| () -> Thread.sleep(Duration.ofSeconds(20)) | |
| ); | |
| } | |
| /** | |
| * Test interrupting Thread.sleep. | |
| */ | |
| @ParameterizedTest | |
| @MethodSource("longSleepers") | |
| void testSleep5(ThrowingRunnable sleeper) throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread t = Thread.currentThread(); | |
| scheduleInterrupt(t, 100); | |
| try { | |
| sleeper.run(); | |
| fail("sleep was not interrupted"); | |
| } catch (InterruptedException e) { | |
| // interrupt status should be cleared | |
| assertFalse(t.isInterrupted()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test that Thread.sleep does not disrupt parking permit. | |
| */ | |
| @Test | |
| void testSleep6() throws Exception { | |
| VThreadRunner.run(() -> { | |
| LockSupport.unpark(Thread.currentThread()); | |
| long start = millisTime(); | |
| Thread.sleep(1000); | |
| expectDuration(start, /*min*/900, /*max*/20_000); | |
| // check that parking permit was not consumed | |
| LockSupport.park(); | |
| }); | |
| } | |
| /** | |
| * Test that Thread.sleep is not disrupted by unparking thread. | |
| */ | |
| @Test | |
| void testSleep7() throws Exception { | |
| AtomicReference<Exception> exc = new AtomicReference<>(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| try { | |
| long start = millisTime(); | |
| Thread.sleep(1000); | |
| expectDuration(start, /*min*/900, /*max*/20_000); | |
| } catch (Exception e) { | |
| exc.set(e); | |
| } | |
| }); | |
| // attempt to disrupt sleep | |
| for (int i = 0; i < 5; i++) { | |
| Thread.sleep(20); | |
| LockSupport.unpark(thread); | |
| } | |
| thread.join(); | |
| Exception e = exc.get(); | |
| if (e != null) { | |
| throw e; | |
| } | |
| } | |
| /** | |
| * Test Thread.sleep when pinned. | |
| */ | |
| @Test | |
| void testSleep8() throws Exception { | |
| VThreadPinner.runPinned(() -> { | |
| long start = millisTime(); | |
| Thread.sleep(1000); | |
| expectDuration(start, /*min*/900, /*max*/20_000); | |
| }); | |
| } | |
| /** | |
| * Test Thread.sleep when pinned and with interrupt status set. | |
| */ | |
| @Test | |
| void testSleep9() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.interrupt(); | |
| try { | |
| VThreadPinner.runPinned(() -> { | |
| Thread.sleep(2000); | |
| }); | |
| fail("sleep not interrupted"); | |
| } catch (InterruptedException e) { | |
| // expected | |
| assertFalse(me.isInterrupted()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test interrupting Thread.sleep when pinned. | |
| */ | |
| @Test | |
| void testSleep10() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread t = Thread.currentThread(); | |
| scheduleInterrupt(t, 100); | |
| try { | |
| VThreadPinner.runPinned(() -> { | |
| Thread.sleep(20 * 1000); | |
| }); | |
| fail("sleep not interrupted"); | |
| } catch (InterruptedException e) { | |
| // interrupt status should be cleared | |
| assertFalse(t.isInterrupted()); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test Thread.sleep(null). | |
| */ | |
| @Test | |
| void testSleep11() throws Exception { | |
| assertThrows(NullPointerException.class, () -> Thread.sleep(null)); | |
| VThreadRunner.run(() -> { | |
| assertThrows(NullPointerException.class, () -> Thread.sleep(null)); | |
| }); | |
| } | |
| /** | |
| * Returns the current time in milliseconds. | |
| */ | |
| private static long millisTime() { | |
| long now = System.nanoTime(); | |
| return TimeUnit.MILLISECONDS.convert(now, TimeUnit.NANOSECONDS); | |
| } | |
| /** | |
| * Check the duration of a task | |
| * @param start start time, in milliseconds | |
| * @param min minimum expected duration, in milliseconds | |
| * @param max maximum expected duration, in milliseconds | |
| * @return the duration (now - start), in milliseconds | |
| */ | |
| private static void expectDuration(long start, long min, long max) { | |
| long duration = millisTime() - start; | |
| assertTrue(duration >= min, | |
| "Duration " + duration + "ms, expected >= " + min + "ms"); | |
| assertTrue(duration <= max, | |
| "Duration " + duration + "ms, expected <= " + max + "ms"); | |
| } | |
| /** | |
| * Test Thread.xxxContextClassLoader from the current thread. | |
| */ | |
| @Test | |
| void testContextClassLoader1() throws Exception { | |
| ClassLoader loader = new ClassLoader() { }; | |
| VThreadRunner.run(() -> { | |
| Thread t = Thread.currentThread(); | |
| t.setContextClassLoader(loader); | |
| assertTrue(t.getContextClassLoader() == loader); | |
| }); | |
| } | |
| /** | |
| * Test inheriting initial value of TCCL from platform thread. | |
| */ | |
| @Test | |
| void testContextClassLoader2() throws Exception { | |
| ClassLoader loader = new ClassLoader() { }; | |
| Thread t = Thread.currentThread(); | |
| ClassLoader savedLoader = t.getContextClassLoader(); | |
| t.setContextClassLoader(loader); | |
| try { | |
| VThreadRunner.run(() -> { | |
| assertTrue(Thread.currentThread().getContextClassLoader() == loader); | |
| }); | |
| } finally { | |
| t.setContextClassLoader(savedLoader); | |
| } | |
| } | |
| /** | |
| * Test inheriting initial value of TCCL from virtual thread. | |
| */ | |
| @Test | |
| void testContextClassLoader3() throws Exception { | |
| VThreadRunner.run(() -> { | |
| ClassLoader loader = new ClassLoader() { }; | |
| Thread.currentThread().setContextClassLoader(loader); | |
| VThreadRunner.run(() -> { | |
| assertTrue(Thread.currentThread().getContextClassLoader() == loader); | |
| }); | |
| }); | |
| } | |
| /** | |
| * Test inheriting initial value of TCCL through an intermediate virtual thread. | |
| */ | |
| @Test | |
| void testContextClassLoader4() throws Exception { | |
| ClassLoader loader = new ClassLoader() { }; | |
| Thread t = Thread.currentThread(); | |
| ClassLoader savedLoader = t.getContextClassLoader(); | |
| t.setContextClassLoader(loader); | |
| try { | |
| VThreadRunner.run(() -> { | |
| VThreadRunner.run(() -> { | |
| assertTrue(Thread.currentThread().getContextClassLoader() == loader); | |
| }); | |
| }); | |
| } finally { | |
| t.setContextClassLoader(savedLoader); | |
| } | |
| } | |
| /** | |
| * Test Thread.xxxContextClassLoader when thread does not inherit the | |
| * initial value of inheritable thread locals. | |
| */ | |
| @Test | |
| void testContextClassLoader5() throws Exception { | |
| VThreadRunner.run(() -> { | |
| ClassLoader loader = new ClassLoader() { }; | |
| Thread.currentThread().setContextClassLoader(loader); | |
| int characteristics = VThreadRunner.NO_INHERIT_THREAD_LOCALS; | |
| VThreadRunner.run(characteristics, () -> { | |
| Thread t = Thread.currentThread(); | |
| assertTrue(t.getContextClassLoader() == ClassLoader.getSystemClassLoader()); | |
| t.setContextClassLoader(loader); | |
| assertTrue(t.getContextClassLoader() == loader); | |
| }); | |
| }); | |
| } | |
| /** | |
| * Test Thread.setUncaughtExceptionHandler. | |
| */ | |
| @Test | |
| void testUncaughtExceptionHandler1() throws Exception { | |
| class FooException extends RuntimeException { } | |
| var handler = new CapturingUHE(); | |
| Thread thread = Thread.ofVirtual().start(() -> { | |
| Thread me = Thread.currentThread(); | |
| assertTrue(me.getUncaughtExceptionHandler() == me.getThreadGroup()); | |
| me.setUncaughtExceptionHandler(handler); | |
| assertTrue(me.getUncaughtExceptionHandler() == handler); | |
| throw new FooException(); | |
| }); | |
| thread.join(); | |
| assertInstanceOf(FooException.class, handler.exception()); | |
| assertEquals(thread, handler.thread()); | |
| assertNull(thread.getUncaughtExceptionHandler()); | |
| } | |
| /** | |
| * Test default UncaughtExceptionHandler. | |
| */ | |
| @Test | |
| void testUncaughtExceptionHandler2() throws Exception { | |
| class FooException extends RuntimeException { } | |
| var handler = new CapturingUHE(); | |
| Thread.UncaughtExceptionHandler savedHandler = Thread.getDefaultUncaughtExceptionHandler(); | |
| Thread.setDefaultUncaughtExceptionHandler(handler); | |
| Thread thread; | |
| try { | |
| thread = Thread.ofVirtual().start(() -> { | |
| Thread me = Thread.currentThread(); | |
| throw new FooException(); | |
| }); | |
| thread.join(); | |
| } finally { | |
| Thread.setDefaultUncaughtExceptionHandler(savedHandler); // restore | |
| } | |
| assertInstanceOf(FooException.class, handler.exception()); | |
| assertEquals(thread, handler.thread()); | |
| assertNull(thread.getUncaughtExceptionHandler()); | |
| } | |
| /** | |
| * Test Thread and default UncaughtExceptionHandler set. | |
| */ | |
| @Test | |
| void testUncaughtExceptionHandler3() throws Exception { | |
| class FooException extends RuntimeException { } | |
| var defaultHandler = new CapturingUHE(); | |
| var threadHandler = new CapturingUHE(); | |
| Thread.UncaughtExceptionHandler savedHandler = Thread.getDefaultUncaughtExceptionHandler(); | |
| Thread.setDefaultUncaughtExceptionHandler(defaultHandler); | |
| Thread thread; | |
| try { | |
| thread = Thread.ofVirtual().start(() -> { | |
| Thread me = Thread.currentThread(); | |
| assertTrue(me.getUncaughtExceptionHandler() == me.getThreadGroup()); | |
| me.setUncaughtExceptionHandler(threadHandler); | |
| assertTrue(me.getUncaughtExceptionHandler() == threadHandler); | |
| throw new FooException(); | |
| }); | |
| thread.join(); | |
| } finally { | |
| Thread.setDefaultUncaughtExceptionHandler(savedHandler); // restore | |
| } | |
| assertInstanceOf(FooException.class, threadHandler.exception()); | |
| assertNull(defaultHandler.exception()); | |
| assertEquals(thread, threadHandler.thread()); | |
| assertNull(thread.getUncaughtExceptionHandler()); | |
| } | |
| /** | |
| * Test no Thread or default UncaughtExceptionHandler set. | |
| */ | |
| @Test | |
| void testUncaughtExceptionHandler4() throws Exception { | |
| Thread.UncaughtExceptionHandler savedHandler = Thread.getDefaultUncaughtExceptionHandler(); | |
| Thread.setDefaultUncaughtExceptionHandler(null); | |
| try { | |
| class FooException extends RuntimeException { } | |
| Thread thread = Thread.ofVirtual().start(() -> { | |
| throw new FooException(); | |
| }); | |
| thread.join(); | |
| assertNull(thread.getUncaughtExceptionHandler()); | |
| } finally { | |
| Thread.setDefaultUncaughtExceptionHandler(savedHandler); | |
| } | |
| } | |
| /** | |
| * Test Thread::threadId and getId. | |
| */ | |
| @Test | |
| void testThreadId1() throws Exception { | |
| record ThreadIds(long threadId, long id) { } | |
| var ref = new AtomicReference<ThreadIds>(); | |
| Thread vthread = Thread.ofVirtual().unstarted(() -> { | |
| Thread thread = Thread.currentThread(); | |
| ref.set(new ThreadIds(thread.threadId(), thread.getId())); | |
| LockSupport.park(); | |
| }); | |
| // unstarted | |
| long tid = vthread.threadId(); | |
| // running | |
| ThreadIds tids; | |
| vthread.start(); | |
| try { | |
| while ((tids = ref.get()) == null) { | |
| Thread.sleep(10); | |
| } | |
| assertTrue(tids.threadId() == tid); | |
| assertTrue(tids.id() == tid); | |
| } finally { | |
| LockSupport.unpark(vthread); | |
| vthread.join(); | |
| } | |
| // terminated | |
| assertTrue(vthread.threadId() == tid); | |
| assertTrue(vthread.getId() == tid); | |
| } | |
| /** | |
| * Test that each Thread has a unique ID | |
| */ | |
| @Test | |
| void testThreadId2() throws Exception { | |
| // thread ID should be unique | |
| long tid1 = Thread.ofVirtual().unstarted(() -> { }).threadId(); | |
| long tid2 = Thread.ofVirtual().unstarted(() -> { }).threadId(); | |
| long tid3 = Thread.currentThread().threadId(); | |
| assertFalse(tid1 == tid2); | |
| assertFalse(tid1 == tid3); | |
| assertFalse(tid2 == tid3); | |
| } | |
| /** | |
| * Test Thread::getState when thread is new/unstarted. | |
| */ | |
| @Test | |
| void testGetState1() { | |
| var thread = Thread.ofVirtual().unstarted(() -> { }); | |
| assertEquals(Thread.State.NEW, thread.getState()); | |
| } | |
| /** | |
| * Test Thread::getState when thread is terminated. | |
| */ | |
| @Test | |
| void testGetState2() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { }); | |
| thread.join(); | |
| assertEquals(Thread.State.TERMINATED, thread.getState()); | |
| } | |
| /** | |
| * Test Thread::getState when thread is runnable (mounted). | |
| */ | |
| @Test | |
| void testGetState3() throws Exception { | |
| var started = new CountDownLatch(1); | |
| var done = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| started.countDown(); | |
| // spin until done | |
| while (!done.get()) { | |
| Thread.onSpinWait(); | |
| } | |
| }); | |
| try { | |
| // wait for thread to start | |
| started.await(); | |
| // thread should be runnable | |
| assertEquals(Thread.State.RUNNABLE, thread.getState()); | |
| } finally { | |
| done.set(true); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getState when thread is runnable (not mounted). | |
| */ | |
| @Test | |
| void testGetState4() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| AtomicBoolean completed = new AtomicBoolean(); | |
| try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| Thread thread1 = factory.newThread(() -> { | |
| Thread thread2 = factory.newThread(LockSupport::park); | |
| assertEquals(Thread.State.NEW, thread2.getState()); | |
| // start t2 to make it runnable | |
| thread2.start(); | |
| try { | |
| assertEquals(Thread.State.RUNNABLE, thread2.getState()); | |
| // yield to allow t2 to run and park | |
| Thread.yield(); | |
| assertEquals(Thread.State.WAITING, thread2.getState()); | |
| } finally { | |
| // unpark t2 to make it runnable again | |
| LockSupport.unpark(thread2); | |
| } | |
| // t2 should be runnable (not mounted) | |
| assertEquals(Thread.State.RUNNABLE, thread2.getState()); | |
| completed.set(true); | |
| }); | |
| thread1.start(); | |
| thread1.join(); | |
| } | |
| assertTrue(completed.get() == true); | |
| } | |
| /** | |
| * Test Thread::getState when thread is blocked waiting to enter a monitor. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetState5(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().unstarted(() -> { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| synchronized (lock) { } | |
| }); | |
| } else { | |
| ready.set(true); | |
| synchronized (lock) { } | |
| } | |
| }); | |
| synchronized (lock) { | |
| thread.start(); | |
| awaitTrue(ready); | |
| // wait for thread to block | |
| await(thread, Thread.State.BLOCKED); | |
| } | |
| thread.join(); | |
| } | |
| /** | |
| * Test Thread::getState when thread is waiting in Object.wait. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetState6(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| synchronized (lock) { | |
| try { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| lock.wait(); | |
| }); | |
| } else { | |
| ready.set(true); | |
| lock.wait(); | |
| } | |
| } catch (InterruptedException e) { } | |
| } | |
| }); | |
| try { | |
| // wait for thread to wait | |
| awaitTrue(ready); | |
| await(thread, Thread.State.WAITING); | |
| // notify, thread should block trying to reenter | |
| synchronized (lock) { | |
| lock.notifyAll(); | |
| await(thread, Thread.State.BLOCKED); | |
| } | |
| } finally { | |
| thread.interrupt(); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getState when thread is waiting in Object.wait(millis). | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetState7(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| synchronized (lock) { | |
| try { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| lock.wait(Long.MAX_VALUE); | |
| }); | |
| } else { | |
| ready.set(true); | |
| lock.wait(Long.MAX_VALUE); | |
| } | |
| } catch (InterruptedException e) { } | |
| } | |
| }); | |
| try { | |
| // wait for thread to timed-wait | |
| awaitTrue(ready); | |
| await(thread, Thread.State.TIMED_WAITING); | |
| // notify, thread should block trying to reenter | |
| synchronized (lock) { | |
| lock.notifyAll(); | |
| await(thread, Thread.State.BLOCKED); | |
| } | |
| } finally { | |
| thread.interrupt(); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getState when thread is parked. | |
| */ | |
| @Test | |
| void testGetState8() throws Exception { | |
| var thread = Thread.ofVirtual().start(LockSupport::park); | |
| try { | |
| await(thread, Thread.State.WAITING); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getState when thread is timed parked. | |
| */ | |
| @Test | |
| void testGetState9() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> LockSupport.parkNanos(Long.MAX_VALUE)); | |
| try { | |
| await(thread, Thread.State.TIMED_WAITING); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getState when thread is parked while holding a monitor. | |
| */ | |
| @Test | |
| void testGetState10() throws Exception { | |
| var started = new CountDownLatch(1); | |
| var done = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| started.countDown(); | |
| synchronized (lock) { | |
| while (!done.get()) { | |
| LockSupport.park(); | |
| } | |
| } | |
| }); | |
| try { | |
| // wait for thread to start | |
| started.await(); | |
| // wait for thread to park | |
| await(thread, Thread.State.WAITING); | |
| } finally { | |
| done.set(true); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getState when thread is timed parked while holding a monitor. | |
| */ | |
| @Test | |
| void testGetState11() throws Exception { | |
| var started = new CountDownLatch(1); | |
| var done = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| started.countDown(); | |
| synchronized (lock) { | |
| while (!done.get()) { | |
| LockSupport.parkNanos(Long.MAX_VALUE); | |
| } | |
| } | |
| }); | |
| try { | |
| // wait for thread to start | |
| started.await(); | |
| // wait for thread to park | |
| await(thread, Thread.State.TIMED_WAITING); | |
| } finally { | |
| done.set(true); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::isAlive. | |
| */ | |
| @Test | |
| void testIsAlive1() throws Exception { | |
| // unstarted | |
| var thread = Thread.ofVirtual().unstarted(LockSupport::park); | |
| assertFalse(thread.isAlive()); | |
| // started | |
| thread.start(); | |
| try { | |
| assertTrue(thread.isAlive()); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| // terminated | |
| assertFalse(thread.isAlive()); | |
| } | |
| /** | |
| * Test Thread.holdsLock when lock not held. | |
| */ | |
| @Test | |
| void testHoldsLock1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| var lock = new Object(); | |
| assertFalse(Thread.holdsLock(lock)); | |
| }); | |
| } | |
| /** | |
| * Test Thread.holdsLock when lock held. | |
| */ | |
| @Test | |
| void testHoldsLock2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| var lock = new Object(); | |
| synchronized (lock) { | |
| assertTrue(Thread.holdsLock(lock)); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test Thread.holdsLock when lock held by carrier thread. | |
| */ | |
| @Disabled | |
| @Test | |
| void testHoldsLock3() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| Object lock = new Object(); | |
| // carrier thread runs all tasks while holding the lock | |
| ThreadFactory carrierThreadFactory = task -> Thread.ofPlatform().unstarted(() -> { | |
| synchronized (lock) { | |
| task.run(); | |
| } | |
| }); | |
| try (ExecutorService pool = Executors.newSingleThreadExecutor(carrierThreadFactory)) { | |
| Executor scheduler = task -> pool.submit(task::run); | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| // start virtual that tests if it holds the lock | |
| var result = new AtomicReference<Boolean>(); | |
| Thread vthread = factory.newThread(() -> { | |
| result.set(Thread.holdsLock(lock)); | |
| }); | |
| vthread.start(); | |
| vthread.join(); | |
| boolean holdsLock = result.get(); | |
| assertFalse(holdsLock, "Thread.holdsLock should return false"); | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace on unstarted thread. | |
| */ | |
| @Test | |
| void testGetStackTraceUnstarted() { | |
| var thread = Thread.ofVirtual().unstarted(() -> { }); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(stack.length == 0); | |
| } | |
| /** | |
| * Test Thread::getStackTrace on thread that has been started but has not run. | |
| */ | |
| @Test | |
| void testGetStackTraceStarted() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| Executor scheduler = task -> { }; | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| Thread thread = factory.newThread(() -> { }); | |
| thread.start(); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(stack.length == 0); | |
| } | |
| /** | |
| * Test Thread::getStackTrace on thread that is runnable-mounted. | |
| */ | |
| @Test | |
| void testGetStackTraceRunnableMounted() throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var done = new AtomicBoolean(); | |
| class Foo { | |
| void spinUntilDone() { | |
| ready.set(true); | |
| while (!done.get()) { | |
| Thread.onSpinWait(); | |
| } | |
| } | |
| } | |
| Foo foo = new Foo(); | |
| var thread = Thread.ofVirtual().start(foo::spinUntilDone); | |
| try { | |
| awaitTrue(ready); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(contains(stack, Foo.class.getName() + ".spinUntilDone")); | |
| } finally { | |
| done.set(true); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace on thread that is runnable-unmounted. | |
| */ | |
| @Test | |
| void testGetStackTraceRunnableUnmounted() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| // custom scheduler with one carrier thread | |
| try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) { | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| // start thread1 to park | |
| Thread thread1 = factory.newThread(LockSupport::park); | |
| thread1.start(); | |
| await(thread1, Thread.State.WAITING); | |
| // start thread2 to spin and pin the carrier thread | |
| var started = new AtomicBoolean(); | |
| var done = new AtomicBoolean(); | |
| Thread thread2 = factory.newThread(() -> { | |
| started.set(true); | |
| while (!done.get()) { | |
| Thread.onSpinWait(); | |
| } | |
| }); | |
| thread2.start(); | |
| awaitTrue(started); | |
| try { | |
| // unpark thread1, it should be "stuck" in runnable state | |
| // (the carrier thread is pinned, no other virtual thread can run) | |
| LockSupport.unpark(thread1); | |
| assertEquals(Thread.State.RUNNABLE, thread1.getState()); | |
| // print thread1's stack trace | |
| StackTraceElement[] stack = thread1.getStackTrace(); | |
| assertTrue(contains(stack, "LockSupport.park")); | |
| } finally { | |
| done.set(true); | |
| } | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace on thread blocked on monitor enter. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetStackTraceBlocked(boolean pinned) throws Exception { | |
| class Foo { | |
| void enter() { | |
| synchronized (this) { } | |
| } | |
| } | |
| Foo foo = new Foo(); | |
| var ready = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().unstarted(() -> { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| foo.enter(); | |
| }); | |
| } else { | |
| ready.set(true); | |
| foo.enter(); | |
| } | |
| }); | |
| synchronized (foo) { | |
| thread.start(); | |
| awaitTrue(ready); | |
| // wait for thread to block | |
| await(thread, Thread.State.BLOCKED); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(contains(stack, Foo.class.getName() + ".enter")); | |
| } | |
| thread.join(); | |
| } | |
| /** | |
| * Test Thread::getStackTrace when thread is waiting in Object.wait. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetStackTraceWaiting(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| synchronized (lock) { | |
| try { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| lock.wait(); | |
| }); | |
| } else { | |
| ready.set(true); | |
| lock.wait(); | |
| } | |
| } catch (InterruptedException e) { } | |
| } | |
| }); | |
| try { | |
| // wait for thread to wait | |
| awaitTrue(ready); | |
| await(thread, Thread.State.WAITING); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(contains(stack, "Object.wait")); | |
| } finally { | |
| thread.interrupt(); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace when thread is waiting in timed-Object.wait. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetStackTraceTimedWaiting(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| synchronized (lock) { | |
| try { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| lock.wait(Long.MAX_VALUE); | |
| }); | |
| } else { | |
| ready.set(true); | |
| lock.wait(Long.MAX_VALUE); | |
| } | |
| } catch (InterruptedException e) { } | |
| } | |
| }); | |
| try { | |
| // wait for thread to wait | |
| awaitTrue(ready); | |
| await(thread, Thread.State.TIMED_WAITING); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(contains(stack, "Object.wait")); | |
| } finally { | |
| thread.interrupt(); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace when thread in park. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetStackTraceParked(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var done = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| if (pinned) { | |
| VThreadPinner.runPinned(() -> { | |
| ready.set(true); | |
| while (!done.get()) { | |
| LockSupport.park(); | |
| } | |
| }); | |
| } else { | |
| ready.set(true); | |
| while (!done.get()) { | |
| LockSupport.park(); | |
| } | |
| } | |
| }); | |
| try { | |
| // wait for thread to park | |
| awaitTrue(ready); | |
| await(thread, Thread.State.WAITING); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(contains(stack, "LockSupport.park")); | |
| } finally { | |
| done.set(true); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace when thread in timed-park. | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = { true, false }) | |
| void testGetStackTraceTimedPark(boolean pinned) throws Exception { | |
| var ready = new AtomicBoolean(); | |
| var done = new AtomicBoolean(); | |
| var thread = Thread.ofVirtual().start(() -> { | |
| if (pinned) { | |
| ready.set(true); | |
| VThreadPinner.runPinned(() -> { | |
| while (!done.get()) { | |
| LockSupport.parkNanos(Long.MAX_VALUE); | |
| } | |
| }); | |
| } else { | |
| ready.set(true); | |
| while (!done.get()) { | |
| LockSupport.parkNanos(Long.MAX_VALUE); | |
| } | |
| } | |
| }); | |
| try { | |
| // wait for thread to park | |
| awaitTrue(ready); | |
| await(thread, Thread.State.TIMED_WAITING); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(contains(stack, "LockSupport.parkNanos")); | |
| } finally { | |
| done.set(true); | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test Thread::getStackTrace on terminated thread. | |
| */ | |
| @Test | |
| void testGetStackTraceTerminated() throws Exception { | |
| var thread = Thread.ofVirtual().start(() -> { }); | |
| thread.join(); | |
| StackTraceElement[] stack = thread.getStackTrace(); | |
| assertTrue(stack.length == 0); | |
| } | |
| /** | |
| * Test that Thread.getAllStackTraces does not include virtual threads. | |
| */ | |
| @Test | |
| void testGetAllStackTraces1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Set<Thread> threads = Thread.getAllStackTraces().keySet(); | |
| assertFalse(threads.stream().anyMatch(Thread::isVirtual)); | |
| }); | |
| } | |
| /** | |
| * Test that Thread.getAllStackTraces includes carrier threads. | |
| */ | |
| @Test | |
| void testGetAllStackTraces2() throws Exception { | |
| assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers"); | |
| try (ForkJoinPool pool = new ForkJoinPool(1)) { | |
| AtomicReference<Thread> ref = new AtomicReference<>(); | |
| Executor scheduler = task -> { | |
| pool.submit(() -> { | |
| ref.set(Thread.currentThread()); | |
| task.run(); | |
| }); | |
| }; | |
| ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler); | |
| Thread vthread = factory.newThread(() -> { | |
| synchronized (lock) { | |
| try { | |
| lock.wait(); | |
| } catch (Exception e) { } | |
| } | |
| }); | |
| vthread.start(); | |
| // get carrier Thread | |
| Thread carrier; | |
| while ((carrier = ref.get()) == null) { | |
| Thread.sleep(20); | |
| } | |
| // wait for virtual thread to block in wait | |
| await(vthread, Thread.State.WAITING); | |
| // get all stack traces | |
| Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces(); | |
| // allow virtual thread to terminate | |
| synchronized (lock) { | |
| lock.notifyAll(); | |
| } | |
| vthread.join(); | |
| // stack trace for the carrier thread | |
| StackTraceElement[] stackTrace = map.get(carrier); | |
| assertNotNull(stackTrace); | |
| assertTrue(contains(stackTrace, "java.util.concurrent.ForkJoinPool")); | |
| assertFalse(contains(stackTrace, "java.lang.Object.wait")); | |
| // there should be no stack trace for the virtual thread | |
| assertNull(map.get(vthread)); | |
| } | |
| } | |
| private boolean contains(StackTraceElement[] stack, String expected) { | |
| return Stream.of(stack) | |
| .map(Object::toString) | |
| .anyMatch(s -> s.contains(expected)); | |
| } | |
| /** | |
| * Test Thread::getThreadGroup on virtual thread created by platform thread. | |
| */ | |
| @Test | |
| void testThreadGroup1() throws Exception { | |
| var thread = Thread.ofVirtual().unstarted(LockSupport::park); | |
| var vgroup = thread.getThreadGroup(); | |
| thread.start(); | |
| try { | |
| assertEquals(vgroup, thread.getThreadGroup()); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| assertNull(thread.getThreadGroup()); | |
| } | |
| /** | |
| * Test Thread::getThreadGroup on platform thread created by virtual thread. | |
| */ | |
| @Test | |
| void testThreadGroup2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| ThreadGroup vgroup = Thread.currentThread().getThreadGroup(); | |
| Thread child = new Thread(() -> { }); | |
| ThreadGroup group = child.getThreadGroup(); | |
| assertEquals(vgroup, group); | |
| }); | |
| } | |
| /** | |
| * Test ThreadGroup returned by Thread::getThreadGroup and subgroup | |
| * created with 2-arg ThreadGroup constructor. | |
| */ | |
| @Test | |
| void testThreadGroup3() throws Exception { | |
| var ref = new AtomicReference<ThreadGroup>(); | |
| var thread = Thread.startVirtualThread(() -> { | |
| ref.set(Thread.currentThread().getThreadGroup()); | |
| }); | |
| thread.join(); | |
| ThreadGroup vgroup = ref.get(); | |
| assertEquals(Thread.MAX_PRIORITY, vgroup.getMaxPriority()); | |
| ThreadGroup group = new ThreadGroup(vgroup, "group"); | |
| assertTrue(group.getParent() == vgroup); | |
| assertEquals(Thread.MAX_PRIORITY, group.getMaxPriority()); | |
| vgroup.setMaxPriority(Thread.MAX_PRIORITY - 1); | |
| assertEquals(Thread.MAX_PRIORITY, vgroup.getMaxPriority()); | |
| assertEquals(Thread.MAX_PRIORITY - 1, group.getMaxPriority()); | |
| vgroup.setMaxPriority(Thread.MIN_PRIORITY); | |
| assertEquals(Thread.MAX_PRIORITY, vgroup.getMaxPriority()); | |
| assertEquals(Thread.MIN_PRIORITY, group.getMaxPriority()); | |
| } | |
| /** | |
| * Test ThreadGroup returned by Thread::getThreadGroup and subgroup | |
| * created with 1-arg ThreadGroup constructor. | |
| */ | |
| @Test | |
| void testThreadGroup4() throws Exception { | |
| VThreadRunner.run(() -> { | |
| ThreadGroup vgroup = Thread.currentThread().getThreadGroup(); | |
| assertEquals(Thread.MAX_PRIORITY, vgroup.getMaxPriority()); | |
| ThreadGroup group = new ThreadGroup("group"); | |
| assertEquals(vgroup, group.getParent()); | |
| assertEquals(Thread.MAX_PRIORITY, group.getMaxPriority()); | |
| vgroup.setMaxPriority(Thread.MAX_PRIORITY - 1); | |
| assertEquals(Thread.MAX_PRIORITY, vgroup.getMaxPriority()); | |
| assertEquals(Thread.MAX_PRIORITY - 1, group.getMaxPriority()); | |
| vgroup.setMaxPriority(Thread.MIN_PRIORITY); | |
| assertEquals(Thread.MAX_PRIORITY, vgroup.getMaxPriority()); | |
| assertEquals(Thread.MIN_PRIORITY, group.getMaxPriority()); | |
| }); | |
| } | |
| /** | |
| * Test Thread.enumerate(false). | |
| */ | |
| @Test | |
| void testEnumerate1() throws Exception { | |
| VThreadRunner.run(() -> { | |
| ThreadGroup vgroup = Thread.currentThread().getThreadGroup(); | |
| Thread[] threads = new Thread[100]; | |
| int n = vgroup.enumerate(threads, /*recurse*/false); | |
| assertFalse(Arrays.stream(threads, 0, n).anyMatch(Thread::isVirtual)); | |
| }); | |
| } | |
| /** | |
| * Test Thread.enumerate(true). | |
| */ | |
| @Test | |
| void testEnumerate2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| ThreadGroup vgroup = Thread.currentThread().getThreadGroup(); | |
| Thread[] threads = new Thread[100]; | |
| int n = vgroup.enumerate(threads, /*recurse*/true); | |
| assertFalse(Arrays.stream(threads, 0, n).anyMatch(Thread::isVirtual)); | |
| }); | |
| } | |
| /** | |
| * Test equals and hashCode. | |
| */ | |
| @Test | |
| void testEqualsAndHashCode() throws Exception { | |
| Thread vthread1 = Thread.ofVirtual().unstarted(LockSupport::park); | |
| Thread vthread2 = Thread.ofVirtual().unstarted(LockSupport::park); | |
| // unstarted | |
| assertTrue(vthread1.equals(vthread1)); | |
| assertTrue(vthread2.equals(vthread2)); | |
| assertFalse(vthread1.equals(vthread2)); | |
| assertFalse(vthread2.equals(vthread1)); | |
| int hc1 = vthread1.hashCode(); | |
| int hc2 = vthread2.hashCode(); | |
| vthread1.start(); | |
| vthread2.start(); | |
| try { | |
| // started, maybe running or parked | |
| assertTrue(vthread1.equals(vthread1)); | |
| assertTrue(vthread2.equals(vthread2)); | |
| assertFalse(vthread1.equals(vthread2)); | |
| assertFalse(vthread2.equals(vthread1)); | |
| assertTrue(vthread1.hashCode() == hc1); | |
| assertTrue(vthread2.hashCode() == hc2); | |
| } finally { | |
| LockSupport.unpark(vthread1); | |
| LockSupport.unpark(vthread2); | |
| } | |
| vthread1.join(); | |
| vthread2.join(); | |
| // terminated | |
| assertTrue(vthread1.equals(vthread1)); | |
| assertTrue(vthread2.equals(vthread2)); | |
| assertFalse(vthread1.equals(vthread2)); | |
| assertFalse(vthread2.equals(vthread1)); | |
| assertTrue(vthread1.hashCode() == hc1); | |
| assertTrue(vthread2.hashCode() == hc2); | |
| } | |
| /** | |
| * Test toString on unstarted thread. | |
| */ | |
| @Test | |
| void testToString1() { | |
| Thread thread = Thread.ofVirtual().unstarted(() -> { }); | |
| thread.setName("fred"); | |
| assertTrue(thread.toString().contains("fred")); | |
| } | |
| /** | |
| * Test toString on running thread. | |
| */ | |
| @Test | |
| void testToString2() throws Exception { | |
| VThreadRunner.run(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.setName("fred"); | |
| assertTrue(me.toString().contains("fred")); | |
| }); | |
| } | |
| /** | |
| * Test toString on parked thread. | |
| */ | |
| @Test | |
| void testToString3() throws Exception { | |
| Thread thread = Thread.ofVirtual().start(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.setName("fred"); | |
| LockSupport.park(); | |
| }); | |
| await(thread, Thread.State.WAITING); | |
| try { | |
| assertTrue(thread.toString().contains("fred")); | |
| } finally { | |
| LockSupport.unpark(thread); | |
| thread.join(); | |
| } | |
| } | |
| /** | |
| * Test toString on terminated thread. | |
| */ | |
| @Test | |
| void testToString4() throws Exception { | |
| Thread thread = Thread.ofVirtual().start(() -> { | |
| Thread me = Thread.currentThread(); | |
| me.setName("fred"); | |
| }); | |
| thread.join(); | |
| assertTrue(thread.toString().contains("fred")); | |
| } | |
| /** | |
| * Thread.UncaughtExceptionHandler that captures the first exception thrown. | |
| */ | |
| private static class CapturingUHE implements Thread.UncaughtExceptionHandler { | |
| Thread thread; | |
| Throwable exception; | |
| @Override | |
| public void uncaughtException(Thread t, Throwable e) { | |
| synchronized (this) { | |
| if (thread == null) { | |
| this.thread = t; | |
| this.exception = e; | |
| } | |
| } | |
| } | |
| Thread thread() { | |
| synchronized (this) { | |
| return thread; | |
| } | |
| } | |
| Throwable exception() { | |
| synchronized (this) { | |
| return exception; | |
| } | |
| } | |
| } | |
| /** | |
| * Waits for the boolean value to become true. | |
| */ | |
| private static void awaitTrue(AtomicBoolean ref) throws Exception { | |
| while (!ref.get()) { | |
| Thread.sleep(20); | |
| } | |
| } | |
| /** | |
| * Waits for the given thread to reach a given state. | |
| */ | |
| private void await(Thread thread, Thread.State expectedState) throws InterruptedException { | |
| Thread.State state = thread.getState(); | |
| while (state != expectedState) { | |
| assertTrue(state != Thread.State.TERMINATED, "Thread has terminated"); | |
| Thread.sleep(10); | |
| state = thread.getState(); | |
| } | |
| } | |
| /** | |
| * Schedule a thread to be interrupted after a delay. | |
| */ | |
| private void scheduleInterrupt(Thread thread, long delayInMillis) { | |
| scheduler.schedule(thread::interrupt, delayInMillis, TimeUnit.MILLISECONDS); | |
| } | |
| } |