Abstract: In this newsletter, we explore how we can visit the entire hierarchy of threads in our virtual machine, including virtual threads. We do this using the composite, visitor and facade design patterns.
Welcome to the 323rd edition of The Java(tm) Specialists' Newsletter. Before I was born, my father drove a sporty white BMW that he had shipped from Germany to Cape Town. He ran it on 120 octane Avgas small aircraft fuel to give it even more oomph. He swapped that beauty for a yellow Volkswagen Beetle and a red Volkswagen Combi bus. He stayed on Volkswagen for the rest of his life, except for a blue Mazda 323 hatchback. 323 - like our newsletter issue. All our Volkswagens gave us endless trouble, but our Mazda ran and ran. My father let me borrow it for the last three years of my studies. He had bought himself a stunning new white Volkswagen Jetta with black leather seats. Our Mazda bellowed smoke and used a pint of engine oil per tank. Every couple of months I had to replace the spark plugs. But it kept limping on past 360,000 km, that old 323.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
In my last newsletter, we showed a code snippet that interrupted all threads. Cay Horstmann pointed out that we were only interrupting the platform threads, not the virtual threads. This brought me to the question - how could we also interrupt the virtual threads?
Unfortunately there is no standard API for iterating through the tree of
threads and thread containers. There is a class
jdk.internal.vm.ThreadContainer
, which we obviously should not
be using, because it is in the jdk.internal
package.
We would have to add the
--add-exports java.base/jdk.internal.vm=ALL-UNNAMED
to our
compiler and runtime, plus our code would no longer necessarily work in
future.
Phew, now that the disclaimer is out of the way, let's put some lipstick on that pig :-)
Oh one more thing. In this newsletter we are going to use the visitor, composite and facade patterns. In case you'd like to study them a bit deeper, have a look at my Design Patterns Course.
The ThreadContainer is similar to the composite design pattern, in that it is a part-whole hierarchy. The ThreadContainer is a composite, containing threads and other ThreadContainers. A ThreadContainer would hold, for example, a StructuredTaskScope or an ExecutorService. We can iterate through all the threads, both virtual and platform, like this:
import jdk.internal.vm.*; import java.util.stream.*; public class ShowALLThreads { public static void main(String... args) { Thread.startVirtualThread(() -> { System.out.println("I'm a virtual thread"); while (true) ; }); // print ALL threads threads(ThreadContainers.root()).forEach(System.out::println); } public static Stream<Thread> threads(ThreadContainer container) { return Stream.concat(container.threads(), container.children().flatMap(ShowALLThreads::threads)); } }
We see both the VirtualThread[#21] and its carrier thread ForkJoinPool-1-worker-1 in the output:
I'm a virtual thread Thread[#1,main,5,main] Thread[#9,Reference Handler,10,system] Thread[#10,Finalizer,8,system] Thread[#11,Signal Dispatcher,9,system] Thread[#18,Common-Cleaner,8,InnocuousThreadGroup] Thread[#19,Monitor Ctrl-Break,5,main] Thread[#20,Notification Thread,9,system] VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1 Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads] Thread[#23,ForkJoinPool-1-worker-2,5,CarrierThreads]
Unfortunately we lose the part-whole structure of the ThreadContainer and Thread. The composite pattern is a close cousin of the visitor pattern, so it makes sense to implement both. First we create the Threadable interface:
public sealed interface Threadable permits ThreadableComposite, ThreadableLeaf { void accept(ThreadableVisitor visitor); }
Before we continue, I played around a bit with the ThreadContainers. The
Stream<Thread>
is a concatenation of the platform threads
and the virtual threads within the container. The platform threads use a
snapshot iterator, and may include threads which are no longer alive. If
new platform threads are started, they will not be seen in the stream.
The virtual threads, on the other hand, are in a concurrent hash set, with
a weakly-consistent iteration. Virtual threads that are no longer alive
since we created the stream are excluded through a filter, and new virtual
threads that have been added to the ThreadContainer might be seen in the
stream. We can witness the difference when we use the jcmd Thread.dump_to_file
command, but it is very subtle and I won't demonstrate that here.
Let's have a look at our ThreadableVisitor
interface,
which has visit methods for the composite (ThreadableComposite
)
and leaf (ThreadableLeaf
), plus the various types of
thread contained inside the ThreadableLeaf
.
public interface ThreadableVisitor { default void visit(ThreadableComposite composite) { composite.children().forEach( threadable -> threadable.accept(this)); } default void visit(ThreadableLeaf leaf) {} default void visit(Thread thread) {} default void visitVirtualThread(Thread thread) { visit(thread); } default void visitPlatformThread(Thread thread) { visit(thread); } }
Our ThreadableComposite
wraps the
ThreadContainer
. The most important method is
accept(ThreadableVisitor)
, but we also make the
children()
method public
in case someone wants to iterate directly:
import jdk.internal.vm.*; import java.util.stream.*; public final class ThreadableComposite implements Threadable { private final ThreadContainer threadContainer; public ThreadableComposite(ThreadContainer threadContainer) { this.threadContainer = threadContainer; } public Stream<Threadable> children() { return Stream.concat( threadContainer.threads().map(ThreadableLeaf::new), threadContainer.children().map(ThreadableComposite::new) ); } public void accept(ThreadableVisitor visitor) { visitor.visit(this); } public String toString() { return threadContainer + " (" + threadContainer.getClass() + ")"; } }
In our ThreadableLeaf
we wrap the Thread
instance with a WeakReference
, so that our wrapper won't
stop the thread from being garbage collected:
import java.lang.ref.*; import java.util.*; public final class ThreadableLeaf implements Threadable { private final Reference<Thread> thread; public ThreadableLeaf(Thread thread) { this.thread = new WeakReference<>(thread); } public void accept(ThreadableVisitor visitor) { var thread = this.thread.get(); if (thread != null && thread.isAlive()) { visitor.visit(this); if (thread.isVirtual()) visitor.visitVirtualThread(thread); else visitor.visitPlatformThread(thread); } } public String toString() { return Objects.toString(thread.get()); } }
Here is an example of a visitor that prints out all the threads, preserving the hierarchical structure:
public class PrintingThreadableVisitor implements ThreadableVisitor { private int indentLevel = 0; public void visit(ThreadableComposite composite) { printIndent(); System.out.println(composite + ": {"); indentLevel++; try { ThreadableVisitor.super.visit(composite); } finally { indentLevel--; } printIndent(); System.out.println("}"); } public void visitVirtualThread(Thread thread) { printIndent(); System.out.println("- Virtual thread: " + thread); } public void visitPlatformThread(Thread thread) { printIndent(); System.out.println("- Platform thread: " + thread); } private void printIndent() { System.out.print(" ".repeat(indentLevel * 4)); } }
To make our Threadable easier to use, we add a Threadables Facade:
import jdk.internal.vm.*; import java.util.function.*; import java.util.stream.*; // Facade for Threadables public class Threadables { private Threadables() {} private static final ThreadContainer ROOT_CONTAINER = ThreadContainers.root(); /** * Return a Threadable of the entire threads tree. */ public static Threadable create() { return new ThreadableComposite(ROOT_CONTAINER); } /** * Iterate througn the threads tree and apply the action to each thread. */ public static void forEach(Consumer<Thread> consumer) { create().accept(new ThreadableVisitor() { public void visit(Thread thread) { consumer.accept(thread); } }); } /** * Create a flat stream of the raw Threads, not preserving the composite. */ public static Stream<Thread> stream() { return stream(ROOT_CONTAINER); } // Input by Cay Horstmann on how to create a concatenation of these streams private static Stream<Thread> stream(ThreadContainer container) { return Stream.concat(container.threads(), container.children().flatMap(Threadables::stream)); } }
Here is an example how we could use the Threadables facade and the PrintingThreadableVisitor::
import java.util.concurrent.*; import java.util.concurrent.locks.*; // Compile and run with --add-exports java.base/jdk.internal.vm=ALL-UNNAMED public class PrintingThreadsWithVisitor { public static void main(String... args) throws Exception { new PrintingThreadsWithVisitor().test(); } private void test() throws Exception { Thread.ofPlatform() .name("Sleeping Platform Thread") .start(this::shortSleep); Thread.ofVirtual() .name("Sleeping Virtual Thread") .start(this::shortSleep); Threadable root; try (var fixedPool = Executors.newFixedThreadPool(2); var scope = new StructuredTaskScope.ShutdownOnFailure()) { fixedPool.submit(this::shortSleep); scope.fork(this::shortSleep); scope.fork(() -> { try (var inner = new StructuredTaskScope.ShutdownOnFailure()) { inner.fork(this::shortSleep); inner.join().throwIfFailed(); return null; } }); root = Threadables.create(); System.out.println("Pretty print threads:"); root.accept(new PrintingThreadableVisitor()); shortSleep(); shortSleep(); // two should be enough for sleeps to be done scope.join().throwIfFailed(); } System.out.println("Pretty print threads again:"); root.accept(new PrintingThreadableVisitor()); } private Void shortSleep() { LockSupport.parkNanos(500_000_000L); return null; // return something so we can use it as Callable in fork() } }
The output is a bit wide, so you might need to read it on your large monitor. Hopefully you will get the idea of the structure of the threadables tree:
Pretty print threads: <root> (class jdk.internal.vm.ThreadContainers$RootContainer$TrackingRootContainer): { - Platform thread: Thread[#3,main,5,main] - Platform thread: Thread[#14,Reference Handler,10,system] - Platform thread: Thread[#15,Finalizer,8,system] - Platform thread: Thread[#16,Signal Dispatcher,9,system] - Platform thread: Thread[#23,Common-Cleaner,8,InnocuousThreadGroup] - Platform thread: Thread[#24,Notification Thread,9,system] - Platform thread: Thread[#25,Sleeping Platform Thread,5,main] - Platform thread: Thread[#26,VirtualThread-unblocker,5,InnocuousThreadGroup] - Virtual thread: VirtualThread[#27,Sleeping Virtual Thread]/timed_waiting java.util.concurrent.ScheduledThreadPoolExecutor@54bedef2 (class jdk.internal.vm.SharedThreadContainer): { - Platform thread: Thread[#29,VirtualThread-unparker,5,InnocuousThreadGroup] } ForkJoinPool-1/jdk.internal.vm.SharedThreadContainer@5fd0d5ae (class jdk.internal.vm.SharedThreadContainer): { - Platform thread: Thread[#28,ForkJoinPool-1-worker-1,5,CarrierThreads] - Platform thread: Thread[#34,ForkJoinPool-1-worker-2,5,CarrierThreads] } ForkJoinPool.commonPool/jdk.internal.vm.SharedThreadContainer@2d98a335 (class jdk.internal.vm.SharedThreadContainer): { } java.util.concurrent.ThreadPoolExecutor@7adf9f5f (class jdk.internal.vm.SharedThreadContainer): { - Platform thread: Thread[#30,pool-1-thread-1,5,main] } java.util.concurrent.ScheduledThreadPoolExecutor@27716f4 (class jdk.internal.vm.SharedThreadContainer): { } java.util.concurrent.StructuredTaskScope$ShutdownOnFailure@6b884d57 (class jdk.internal.misc.ThreadFlock$ThreadContainerImpl): { - Virtual thread: VirtualThread[#32]/waiting - Virtual thread: VirtualThread[#31]/timed_waiting java.util.concurrent.StructuredTaskScope$ShutdownOnFailure@1dc41efe (class jdk.internal.misc.ThreadFlock$ThreadContainerImpl): { - Virtual thread: VirtualThread[#33]/timed_waiting } } } Pretty print threads again: <root> (class jdk.internal.vm.ThreadContainers$RootContainer$TrackingRootContainer): { - Platform thread: Thread[#3,main,5,main] - Platform thread: Thread[#14,Reference Handler,10,system] - Platform thread: Thread[#15,Finalizer,8,system] - Platform thread: Thread[#16,Signal Dispatcher,9,system] - Platform thread: Thread[#23,Common-Cleaner,8,InnocuousThreadGroup] - Platform thread: Thread[#24,Notification Thread,9,system] - Platform thread: Thread[#26,VirtualThread-unblocker,5,InnocuousThreadGroup] java.util.concurrent.ScheduledThreadPoolExecutor@54bedef2 (class jdk.internal.vm.SharedThreadContainer): { - Platform thread: Thread[#29,VirtualThread-unparker,5,InnocuousThreadGroup] } ForkJoinPool-1/jdk.internal.vm.SharedThreadContainer@5fd0d5ae (class jdk.internal.vm.SharedThreadContainer): { - Platform thread: Thread[#28,ForkJoinPool-1-worker-1,5,CarrierThreads] - Platform thread: Thread[#34,ForkJoinPool-1-worker-2,5,CarrierThreads] } ForkJoinPool.commonPool/jdk.internal.vm.SharedThreadContainer@2d98a335 (class jdk.internal.vm.SharedThreadContainer): { } java.util.concurrent.ScheduledThreadPoolExecutor@27716f4 (class jdk.internal.vm.SharedThreadContainer): { } }
Since the SharedThreadContainer uses an unordered concurrent data structure, the order in which elements are streamed is not always going to be the same. If this is important, we could write a sorted ThreadableVisitor, which would return a sorted stream of children. However, if we do that, we will also lose the weakly consistent characteristic of the stream.
We can write our own custom visitor by implementing ThreadableVisitor
.
For example, in this demo we make a visitor that interrupts all the threads:
import java.util.concurrent.*; // Compile and run with --add-exports java.base/jdk.internal.vm=ALL-UNNAMED public class InterruptAllThreadsIncludingVirtual { public static void main(String... args) throws Exception { var interrupter = new ThreadableVisitor() { public void visit(Thread thread) { thread.interrupt(); } }; var sleepingVirtualThread = Thread.ofVirtual() .name("Sleeping Virtual Thread").start(() -> { try { Thread.sleep(10_000); } catch (InterruptedException e) { System.out.println("Virtual thread sleep interrupted"); } }); try (var fixedPool = Executors.newFixedThreadPool(2); var scope = new StructuredTaskScope.ShutdownOnFailure()) { fixedPool.submit(() -> { try { Thread.sleep(10_000); } catch (InterruptedException e) { System.out.println("Fixed thread sleep interrupted"); } return "done"; }); scope.fork(() -> { try { Thread.sleep(10_000); } catch (InterruptedException e) { System.out.println("Forked sleep interrupted"); } return "done"; }); scope.fork(() -> { try (var inner = new StructuredTaskScope.ShutdownOnFailure()) { inner.fork(() -> { try { Thread.sleep(10_000); } catch (InterruptedException e) { System.out.println("Inner forked sleep interrupted"); } return "done"; }); try { inner.join().throwIfFailed(); } catch (InterruptedException e) { System.out.println("Inner scope interrupted"); } return "done"; } }); Thread.sleep(100); Threadables.create().accept(interrupter); System.out.println("All threads interrupted"); Thread.interrupted(); // clear my interrupt scope.join().throwIfFailed(); sleepingVirtualThread.join(); } } }
Again, we need to compile and run this with the flag
--add-exports java.base/jdk.internal.vm=ALL-UNNAMED
and then we
should see:
Fixed thread sleep interrupted Virtual thread sleep interrupted All threads interrupted Forked sleep interrupted Inner scope interrupted Inner forked sleep interrupted
ThreadMXBean#findDeadlockedThreads() unfortunately does not work for virtual threads. If we want to look for deadlocks, we need to do it ourselves. Here is a quick trick to look for BLOCKED threads, although they may not necessarily be deadlocked:
import java.lang.management.*; import java.util.*; import java.util.concurrent.locks.*; import java.util.stream.*; public class DeadlockDemo { public static void main(String... args) { var lock1 = new Object(); var lock2 = new Object(); Thread.startVirtualThread(() -> lockBoth(lock1, lock2)); Thread.startVirtualThread(() -> lockBoth(lock2, lock1)); LockSupport.parkNanos(100_000_000L); System.out.println("Find all blocked threads"); Threadables.stream() .filter(thread -> thread.getState() == Thread.State.BLOCKED) .forEach(thread -> { System.out.println(thread + " " + thread.getState()); Stream.of(thread.getStackTrace()) .map(element -> "\t" + element) .forEach(System.out::println); }); } private static void lockBoth(Object first, Object second) { synchronized (first) { System.out.println("First locked"); LockSupport.parkNanos(50_000_000L); synchronized (second) { System.out.println("Both locked"); } } } }
Output is the following:
First locked First locked Find all blocked threads VirtualThread[#27]/blocked BLOCKED DeadlockDemo.lockBoth(DeadlockDemo.java:36) DeadlockDemo.lambda$main$0(DeadlockDemo.java:15) java.base/java.lang.VirtualThread.run(VirtualThread.java:466) VirtualThread[#30]/blocked BLOCKED DeadlockDemo.lockBoth(DeadlockDemo.java:36) DeadlockDemo.lambda$main$1(DeadlockDemo.java:16) java.base/java.lang.VirtualThread.run(VirtualThread.java:466)
Whilst not proving a deadlock, it is a first step to start searching. Prior to Java 24, the carrier threads would also be BLOCKED.
Kind regards
Heinz
P.S. Besides our self-study Java Concurrency Aficionados 2024, we also teach in-house courses to companies, either in person or live virtual. Please contact me via email on heinz@javaspecialists.eu and I will be happy to help you figure out what works best for you.
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.