Running on Java 24+36-3646 (Preview)
Home of The JavaSpecialists' Newsletter

323Visiting All Threads (Including Virtual)

Author: Dr Heinz M. KabutzDate: 2025-03-23Java Version: 24Category: Concurrency
 

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.

Iterating All Threads (Including Virtual)

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.

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '24

Superpack '24 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...