Processes, Threads, Coroutines, and Virtual Threads: A Deep Dive into Java Concurrency

This article explains the differences and relationships between processes, threads, coroutines, and virtual threads, provides detailed Java code examples, compares their characteristics and performance, and offers practical guidance on choosing the right concurrency model for various scenarios.

IT Services Circle
IT Services Circle
IT Services Circle
Processes, Threads, Coroutines, and Virtual Threads: A Deep Dive into Java Concurrency

Preface

Virtual threads have become popular recently, greatly improving system performance, but many developers still confuse processes, threads, coroutines, and virtual threads. This article clarifies these concepts.

1. Processes and Threads

Processes are independent execution environments with their own address space, stack, and resources, similar to separate workshops in a factory. Threads are workers within a process that share the process's resources.

Process characteristics : independent address space, safety (a crash does not affect others), high creation overhead, complex inter‑process communication.

Thread characteristics : lightweight, share memory, simple communication, require synchronization.

// Java example of creating a process
public class ProcessExample {
    public static void main(String[] args) throws IOException {
        ProcessBuilder pb = new ProcessBuilder("calc.exe");
        Process p = pb.start();
        System.out.println("Process ID: " + p.pid());
        System.out.println("Alive: " + p.isAlive());
        int exitCode = p.waitFor();
        System.out.println("Exit code: " + exitCode);
    }
}

Creating Threads

// Three ways to create a thread in Java
public class ThreadExample {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.start();
        Thread t2 = new Thread(new MyRunnable());
        t2.start();
        Thread t3 = new Thread(() -> {
            System.out.println("Lambda thread running: " + Thread.currentThread().getName());
        });
        t3.start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread running: " + Thread.currentThread().getName());
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable running: " + Thread.currentThread().getName());
    }
}

2. Deep Dive into Threads

Modern operating systems use three thread models:

1. User‑Level Threads (ULT)

Implemented entirely in user space; the OS is unaware of them. Creation, scheduling, and synchronization are handled by a user‑level library.

Advantages: no kernel mode switch, low overhead, customizable scheduling.

Disadvantages: a blocked thread blocks the whole process, cannot exploit multiple CPUs.

2. Kernel‑Level Threads (KLT)

Managed directly by the OS kernel; each kernel thread maps to a scheduling entity.

Advantages: a blocked thread does not block others, can run on multiple cores.

Disadvantages: higher context‑switch cost, requires system calls.

3. Hybrid Model

Most modern OSes use a hybrid model that maps user‑level threads onto kernel threads. Java threads follow this model.

3. Coroutines

Coroutines are even lighter than threads and are scheduled by the programmer in user space.

Extremely lightweight – millions can be created.

Cooperative scheduling – explicit yield points.

Low‑cost switching – no kernel involvement.

Write asynchronous code in a synchronous style.

// Pseudo‑code illustrating coroutine concepts in Java (requires a library like Quasar)
public class CoroutineExample {
    public static void main(String[] args) {
        Coroutine c1 = new Coroutine(() -> {
            System.out.println("Coroutine 1 start");
            Coroutine.yield();
            System.out.println("Coroutine 1 resume");
        });
        Coroutine c2 = new Coroutine(() -> {
            System.out.println("Coroutine 2 start");
            Coroutine.yield();
            System.out.println("Coroutine 2 resume");
        });
        c1.run();
        c2.run();
        c1.run();
        c2.run();
    }
}

4. Virtual Threads

Java 19 introduced virtual threads, a major breakthrough in the Java concurrency model. They address the limitations of platform threads.

Why Virtual Threads?

Traditional threads suffer from limited thread count, large memory overhead (default 1 MB stack), and expensive context switches.

Implementation Principle

Virtual threads are lightweight Java‑level threads scheduled onto a small pool of platform threads. When a virtual thread blocks, the JVM detaches it from the platform thread, allowing the platform thread to run other virtual threads.

// Simple example of creating a virtual thread
public class VirtualThreadExample {
    public static void main(String[] args) throws InterruptedException {
        Thread vt = Thread.ofVirtual().start(() -> {
            System.out.println("Virtual thread running: " + Thread.currentThread());
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        });
        vt.join();
    }
}

Advantages

Lightweight – millions can be created.

Low‑cost blocking – blocking does not block the platform thread.

Simplified concurrency – write blocking code in a synchronous style.

Compatibility – virtual threads implement Thread, so existing APIs work.

5. Choosing the Right Model

CPU‑Intensive Tasks

Use a fixed‑size platform thread pool equal to the number of CPU cores.

// CPU‑intensive task example
public class CpuIntensiveTask {
    public static void main(String[] args) throws InterruptedException {
        int processors = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(processors);
        for (int i = 0; i < 100; i++) {
            executor.submit(() -> compute());
        }
        executor.shutdown();
    }
    private static void compute() {
        long result = 0;
        for (long i = 0; i < 100_000_000L; i++) {
            result += i * i;
        }
        System.out.println("Result: " + result);
    }
}

IO‑Intensive Tasks

Virtual threads shine here because they do not occupy a platform thread while waiting for IO.

// IO‑intensive task using virtual threads
public class IoIntensiveTask {
    public static void main(String[] args) throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                executor.submit(() -> {
                    String data = httpGet("https://api.example.com/data");
                    processData(data);
                });
            }
        }
    }
    private static String httpGet(String url) {
        try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
        return "response data";
    }
    private static void processData(String data) {
        System.out.println("Processing: " + data);
    }
}

Mixed Workloads

Combine virtual threads for IO and a fixed thread pool for CPU work.

// Mixed workload example
public class MixedTask {
    public static void main(String[] args) throws InterruptedException {
        try (var ioExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                futures.add(ioExecutor.submit(() -> fetchData()));
            }
            int processors = Runtime.getRuntime().availableProcessors();
            ExecutorService cpuExecutor = Executors.newFixedThreadPool(processors);
            for (Future<String> f : futures) {
                cpuExecutor.submit(() -> {
                    try {
                        String data = f.get();
                        processDataIntensively(data);
                    } catch (Exception e) { e.printStackTrace(); }
                });
            }
            cpuExecutor.shutdown();
        }
    }
    private static String fetchData() { return "data"; }
    private static void processDataIntensively(String d) { /* heavy CPU work */ }
}

6. Performance Comparison

A benchmark shows that handling 10 000 IO‑bound tasks with a 200‑thread pool takes about 50 seconds, while using virtual threads completes in roughly 1 second—a 50× speedup.

// Benchmark comparing platform threads and virtual threads
public class PerformanceComparison {
    private static final int TASK_COUNT = 10_000;
    private static final int IO_DELAY_MS = 100;
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        testPlatformThreads();
        long platform = System.currentTimeMillis() - start;
        start = System.currentTimeMillis();
        testVirtualThreads();
        long virtual = System.currentTimeMillis() - start;
        System.out.println("Platform time: " + platform + "ms");
        System.out.println("Virtual time: " + virtual + "ms");
        System.out.println("Speedup: " + ((double) platform / virtual) + "x");
    }
    private static void testPlatformThreads() throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(200);
        CountDownLatch latch = new CountDownLatch(TASK_COUNT);
        for (int i = 0; i < TASK_COUNT; i++) {
            exec.submit(() -> {
                try { Thread.sleep(IO_DELAY_MS); } catch (InterruptedException e) { e.printStackTrace(); }
                finally { latch.countDown(); }
            });
        }
        latch.await();
        exec.shutdown();
    }
    private static void testVirtualThreads() throws InterruptedException {
        try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
            CountDownLatch latch = new CountDownLatch(TASK_COUNT);
            for (int i = 0; i < TASK_COUNT; i++) {
                exec.submit(() -> {
                    try { Thread.sleep(IO_DELAY_MS); } catch (InterruptedException e) { e.printStackTrace(); }
                    finally { latch.countDown(); }
                });
            }
            latch.await();
        }
    }
}

7. Summary

Choosing the appropriate concurrency model depends on isolation needs, workload type, and scalability requirements:

Use separate processes for strong isolation (micro‑services).

Use platform threads for CPU‑bound tasks, matching thread count to CPU cores.

Use virtual threads for IO‑bound workloads to achieve massive concurrency with minimal overhead.

Consider coroutines in languages that provide native support (e.g., Go) when extreme concurrency is required.

Remember: there is no universally best concurrency model—only the one that best fits your specific problem.

8. Future Outlook

Virtual threads are a major step forward, but further evolution is expected:

Better tooling for debugging and monitoring virtual threads.

More intelligent scheduling algorithms.

Integration with reactive and actor models.

Hardware‑level optimizations (e.g., DPU cooperation).

Diagram
Diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaCoroutinesVirtualThreadsThreads
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.