Processes, Threads, Coroutines, and Virtual Threads: A Complete Java Concurrency Guide

This article explains the fundamental differences between processes, threads, coroutines, and virtual threads, shows how they are implemented in Java with code examples, compares their performance, and provides practical guidance on choosing the right concurrency model for various workloads.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Processes, Threads, Coroutines, and Virtual Threads: A Complete Java Concurrency Guide

Introduction

Virtual threads have become popular because they dramatically improve system performance, but many developers still confuse processes, threads, coroutines, and virtual threads. This article clarifies the concepts, their relationships, and practical usage in Java.

1. Processes and Threads

In everyday work you may hear the terms "process" and "thread" without fully understanding their essential differences.

Imagine a large factory (the operating system):

Process is like an independent workshop with its own space, raw materials, and tools.

Thread is like a worker inside a workshop, sharing the workshop’s resources to complete tasks.

Process: Independent Execution Environment

A process is the basic unit of resource allocation and scheduling in an OS.

Each process has its own address space, data stack, code segment, and other system resources.

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

Process characteristics :

Independence – each process has an isolated address space.

Safety – a crash in one process does not affect others.

High overhead – creation and destruction consume considerable system resources.

Complex communication – inter‑process communication (IPC) requires special mechanisms.

Thread: Lightweight Execution Unit

A thread is the basic unit of CPU scheduling and execution inside a process.

A process can contain many threads that share the process’s resources.

// Two ways to create a thread in Java
public class ThreadExample {
    public static void main(String[] args) {
        // 1) Extend Thread
        Thread thread1 = new MyThread();
        thread1.start();
        // 2) Implement Runnable
        Thread thread2 = new Thread(new MyRunnable());
        thread2.start();
        // 3) Lambda expression
        Thread thread3 = new Thread(() -> {
            System.out.println("Lambda thread: " + Thread.currentThread().getName());
        });
        thread3.start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread runs: " + Thread.currentThread().getName());
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable runs: " + Thread.currentThread().getName());
    }
}

Thread characteristics :

Shared resources – threads in the same process share memory and system resources.

Lightweight – creation and destruction cost less than processes.

Simple communication – threads can directly read/write shared data.

Safety concerns – synchronization and resource‑sharing issues must be handled.

2. Deep Dive into Thread Models

Modern operating systems usually provide three thread models.

1. User‑Level Threads (ULT)

User‑level threads are 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.

Scheduling algorithm can be customized by the application.

Does not depend on OS support.

Disadvantages :

If one thread blocks, the whole process blocks.

Cannot exploit multi‑core CPUs.

2. Kernel‑Level Threads (KLT)

Kernel‑level threads are directly supported and managed by the OS kernel. Each kernel thread maps to a kernel scheduling entity.

Advantages :

Blocking of one thread does not affect others.

Can run on multiple CPU cores in parallel.

Disadvantages :

Thread switch requires a kernel‑mode transition, higher overhead.

Creating a thread requires a system call.

3. Hybrid Model

Most modern OSes use a hybrid model that maps user‑level threads to kernel‑level threads. Java threads are an implementation of this model.

// Mapping Java threads to OS threads
public class ThreadInfoExample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println("Java thread: " + Thread.currentThread().getName()
                                   + ", OS thread ID: " + getThreadId());
                try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
            }).start();
        }
    }
    private static long getThreadId() {
        return Thread.currentThread().threadId(); // Java 10+
    }
}

3. Coroutines

Coroutines are especially popular in Go, but the concept exists in other languages as well.

Coroutines are lighter than threads and are scheduled by the programmer in user space rather than by the OS kernel.

// Pseudo‑code of a coroutine in Java (using a third‑party library)
public class CoroutineExample {
    public static void main(String[] args) {
        Coroutine c1 = new Coroutine(() -> {
            System.out.println("Coroutine 1 start");
            Coroutine.yield(); // voluntarily give up execution
            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();
    }
}

Coroutine characteristics :

Extremely lightweight – millions can be created.

Cooperative scheduling – the programmer decides when to yield.

Low‑cost switching – no kernel‑mode transition.

Synchronous programming style – asynchronous code can be written synchronously.

Coroutine vs Thread

Comparison diagram (core idea):

Creation‑quantity diagram:

4. Virtual Threads

Java 19 introduced virtual threads, a major breakthrough in the Java concurrency model.

Virtual threads aim to solve the limitations of traditional platform threads.

Why Do We Need Virtual Threads?

When handling massive concurrent requests, creating a large number of platform threads quickly hits bottlenecks:

Thread count limit : OS limits the number of native threads (usually a few thousand).

Memory overhead : each thread reserves a stack (default 1 MiB).

Context‑switch cost : switching requires kernel involvement and is expensive.

Implementation Principle

Virtual threads are lightweight threads implemented by the JDK. They are not scheduled by the OS directly; instead, the JDK schedules them onto platform threads.

// Java 19+ virtual thread example
public class VirtualThreadExample {
    public static void main(String[] args) throws InterruptedException {
        Thread vt = Thread.ofVirtual().start(() -> {
            System.out.println("Virtual thread runs: " + Thread.currentThread());
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        });
        vt.join();
        // Massive task processing with virtual‑thread‑per‑task executor
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("Task " + taskId + " on thread: " + Thread.currentThread());
                    Thread.sleep(1000);
                    return taskId;
                });
            }
        }
    }
}

Advantages of Virtual Threads

Lightweight – millions can be created without exhausting resources.

Low‑cost blocking – a blocked virtual thread does not block the underlying platform thread.

Simplified concurrency – code can be written in a synchronous style.

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

5. How Virtual Threads Work

The key concept is continuation , a resumable execution context.

Continuation Concept

When a virtual thread performs a blocking operation, the JDK suspends its continuation and releases the platform thread for other work.

// Pseudo‑code demonstrating continuation
public class ContinuationExample {
    public static void main(String[] args) {
        ContinuationScope scope = new ContinuationScope("example");
        Continuation cont = new Continuation(scope, () -> {
            System.out.println("Step 1");
            Continuation.yield(scope);
            System.out.println("Step 2");
            Continuation.yield(scope);
            System.out.println("Step 3");
        });
        while (!cont.isDone()) {
            System.out.println("Running step...");
            cont.run();
            System.out.println("Step paused");
        }
    }
}

Scheduling Model

Virtual threads use a ForkJoinPool as the scheduler, mapping many virtual threads onto a small pool of platform threads.

When a virtual thread blocks, the scheduler automatically suspends it and runs another virtual thread on the same platform thread.

This model allows a handful of platform threads to efficiently execute millions of virtual threads, greatly increasing concurrency capacity.

6. Choosing the Right Model for Different Scenarios

Should you always use virtual threads? Not necessarily. Different workloads suit different concurrency models.

1. CPU‑Intensive Tasks

For CPU‑bound work (e.g., calculations, data processing), traditional platform threads are usually more appropriate.

// CPU‑intensive task example
public class CpuIntensiveTask {
    public static void main(String[] args) {
        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);
    }
}

2. I/O‑Intensive Tasks

For I/O‑bound work (network calls, database access), virtual threads provide a clear advantage.

// I/O‑intensive task using virtual threads
public class IoIntensiveTask {
    public static void main(String[] args) {
        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);
                    return null;
                });
            }
        }
    }
    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("Process data: " + data);
    }
}

3. Mixed Workloads

When a task involves both CPU and I/O, combine executors: use virtual threads for I/O parts and a fixed thread pool for CPU‑heavy parts.

// Mixed workload example
public class MixedTask {
    public static void main(String[] args) {
        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(() -> {
                    String data = f.get();
                    processDataIntensively(data);
                });
            }
            cpuExecutor.shutdown();
        } catch (Exception e) { e.printStackTrace(); }
    }
    private static String fetchData() { return "data"; }
    private static void processDataIntensively(String d) { /* CPU work */ }
}

7. Performance Comparison

A simple benchmark demonstrates the performance gap between platform threads and virtual threads for an I/O‑bound workload.

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

Test results analysis :

Platform thread pool (200 threads) processes 10 000 tasks in ~50 seconds.

Virtual threads process the same workload in ~1 second.

Performance improvement: about 50×.

Summary Table

Feature

Process

Thread

Coroutine

Virtual Thread

Isolation

High

Low

Low

Low

Creation overhead

Large

Medium

Small

Very small

Switch overhead

Large

Medium

Small

Small

Memory usage

Large

Medium

Small

Small

Concurrent count

Dozens

Thousands

Hundreds of thousands

Millions

Typical scenario

Isolated services

General concurrency

Language‑specific (e.g., Go)

I/O‑intensive workloads

Choosing Guidelines

Need full isolation : choose processes (e.g., micro‑services).

CPU‑bound tasks : use platform thread pool sized to CPU cores.

I/O‑bound tasks : use virtual threads (Java 19+).

Very high concurrency : consider coroutines (Go) or virtual threads.

Migrating existing systems : gradually introduce virtual threads while keeping API compatibility.

Best Practices

Avoid creating excessive platform threads.

Use appropriate thread pools based on task type.

Try virtual threads for I/O‑heavy scenarios.

Monitor thread states with observability tools.

Understand business characteristics to select the optimal concurrency model.

Future Outlook

Better tool support: debuggers and monitors must adapt to virtual threads.

Smarter scheduling algorithms for different workloads.

Integration with other models such as reactive programming and actor systems.

Hardware‑software co‑optimization (e.g., DPUs) for next‑gen concurrency.

Remember: there is no universally best concurrency model, only the most suitable one for your specific scenario.
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.

JavaprocessVirtual ThreadsCoroutinesThreads
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.