Skip to content

Java Thread vs Virtual Thread

Somkiat Puisungnoen edited this page Jul 15, 2026 · 3 revisions

Java Thread

public class MainThread {
    static void main() {
        System.out.println("Main thread started: " + Thread.currentThread().getName());

        for (int i = 1; i <= 3; i++) {
            Thread platformThread = new Thread(() -> {
                System.out.println("Platform thread " + Thread.currentThread().getName() + " is running.");
                try {
                    Thread.sleep(2000); // Simulate some work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Platform thread " + Thread.currentThread().getName() + " finished.");
            }, "Platform-Thread-" + i);
            platformThread.start();
        }

        System.out.println("Main thread finished.");
    }
}

Virtual Thread

public class MainVT {
    static void main() throws InterruptedException {
        System.out.println("Main thread started: " + Thread.currentThread().getName());

        for (int i = 1; i <= 100; i++) { // Creating many more threads!
            Thread.startVirtualThread(() -> {
                System.out.println("Virtual thread " + Thread.currentThread().getName() + "ID=" + Thread.currentThread().threadId() + " is running.");
                try {
                    Thread.sleep(2000); // Simulate blocking I/O
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Virtual thread " + Thread.currentThread().getName() + "ID=" + Thread.currentThread().threadId() + "  finished.");
            });
        }

        // Give some time for virtual threads to run
        Thread.sleep(3000);
        System.out.println("Main thread finished: " + Thread.currentThread().getName());
    }
}

Create 1 million Virtual Thread

public class MainMillionVT {
    static void main() throws InterruptedException {
        Instant start = Instant.now();
        Set<Long> vThreadIds = new HashSet<>();

        // Create 1 million virtual threads
        var vThreads = IntStream.range(0, 1_000_000).mapToObj(i -> Thread.ofVirtual().unstarted(() -> {
            vThreadIds.add(Thread.currentThread().getId());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        })).toList();

        vThreads.forEach(Thread::start);
        for (var thread : vThreads) {
            thread.join();
        }

        Instant end = Instant.now();
        System.out.println("Time =" + Duration.between(start, end).toMillis() + " ms");
        System.out.println("Number of unique vThreads used " + vThreadIds.size());
    }
}

Structured Concurrency in Java

  • Deterministic Lifetimes (No Zombie Threads)
  • Short-Circuiting Error Handling
  • Streamlined Cancellation
  • Tree-Like Diagnostic Observability

Old way with ExecutorService

// Danger: Lacks structural cleanup guarantees
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<String> futureA = executor.submit(() -> fetchDataA());
    Future<String> futureB = executor.submit(() -> fetchDataB());
    
    // If futureA.get() throws an exception, futureB keeps running blindly
    return futureA.get() + " & " + futureB.get(); 
}

New way with StructuredTaskScope

// Safe: Structured, self-contained unit of work
try (var scope = new StructuredTaskScope<>(SubtaskJoiner.allSuccessful())) {
    
    // Forks subtasks into explicitly tracked virtual threads
    Subtask<String> taskA = scope.fork(() -> fetchDataA());
    Subtask<String> taskB = scope.fork(() -> fetchDataB());

    scope.join(); // Synchronization point: awaits execution or failure
    
    return taskA.get() + " & " + taskB.get();
} // All threads are guaranteed terminated here

Clone this wiki locally