π§© Introduction
With Java 21, Virtual Threads became a powerful tool to simplify writing scalable concurrent applications. But what happens when things go wrong? How do you debug issues in these lightweight threads?
This blog will walk you through the ways of debugging virtual threads using simple examples, helpful tools, and clear explanations that even beginners can follow.

π― What Are Virtual Threads?
Virtual threads are a lightweight version of platform threads. They are:
- Managed by the Java runtime (not the OS)
- Created in huge numbers without harming performance
- Great for handling concurrent tasks
To use them, Java 21 provides the Thread.ofVirtual()
API.
You can go through Introduction to Virtual Threads: Java 21’s Game-Changer for Concurrency for understanding the basic’s.
π§ Why Debugging Virtual Threads Is Different
Traditional debuggers were made for heavyweight threads. With virtual threads:
- There can be thousands of threads
- Stack traces may look different
- Tools may not always display virtual threads by default
But donβt worry β there are ways to handle this!
ποΈ Sample Project Structure
We’ll use this package structure:
1 2 3 4 5 6 7 8 |
src/ βββ com/ βββ kscodes/ βββ Main.java βββ Service.java |
π¨βπ» Step-by-Step Example
File : Service.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.kscodes; public class Service { public void performTask(int id) { System.out.println("Running task " + id + " in thread: " + Thread.currentThread()); try { Thread.sleep(100); // Simulate work } catch (InterruptedException e) { System.out.println("Task " + id + " interrupted!"); } } } |
File : Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.kscodes; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { Service service = new Service(); try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 1; i <= 5; i++) { int id = i; executor.submit(() -> service.performTask(id)); } } } } |
This code runs 5 tasks in virtual threads, each calling a simple method.
π΅οΈββοΈ How to Debug Virtual Threads
β 1. Use IntelliJ IDEA (2023.1+) or VS Code
Modern IDEs like IntelliJ and Visual Studio Code now support debugging virtual threads. Make sure you’re using:
- IntelliJ IDEA 2023.1 or newer
- Java 21 SDK
- Enable “Show all threads” in debugger
β 2. Print Thread Info in Logs
Always print the current thread in your logs:
1 2 |
System.out.println("Thread: " + Thread.currentThread()); |
This helps you trace actions done by each virtual thread.
β 3. Use StackWalker API
Javaβs StackWalker
helps inspect stack traces inside a running thread:
1 2 |
StackWalker.getInstance().forEach(System.out::println); |
This is useful when debugging without an IDE or to trace thread stacks during execution.
β 4. Add Thread Names (Optional)
To name threads for better logs:
1 2 3 |
Thread.ofVirtual().name("worker-" + id).start(() ->; service.performTask(id)); |
Example output:
Running task 1 in thread: VirtualThread[ #29 , worker-1]
β 5. Using JDK Mission Control + Java Flight Recorder
JFR and JMC support virtual threads starting in Java 21.
- Add
-XX:StartFlightRecording
when starting your app - Analyze in JDK Mission Control
This helps visualize thread behavior and performance over time.
π Common Issues & Fixes
Issue | Solution |
---|---|
Logs show many unnamed threads | Use .name("...") when creating virtual threads |
IDE doesnβt show threads | Update IDE & JDK; enable all threads in settings |
Threads not working as expected | Check try-with-resources in executors |
π§ͺ Bonus: Debugging with Breakpoints
You can still set breakpoints like usual in:
1 2 3 4 |
service.performTask(id); |
Even though thousands of threads may run, IDEs like IntelliJ will pause only the one that hits the breakpoint.
β Works just like platform threads!
π§Ή Clean Up After Debugging
Remember to close your virtual thread executors:
1 2 3 4 5 6 |
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { // your tasks here } |
This ensures no hanging threads are left in your app.
π§ Summary
Debugging virtual threads might feel new, but itβs manageable with the right tools and techniques:
β
Use Thread.currentThread()
for clarity
β
Use modern IDEs and enable all-thread views
β
Leverage structured logging and JFR for observability
β
Donβt forget thread naming and try-with-resources cleanup