Understanding Java Virtual Threads: From Traditional Thread Models to Stackful Coroutines
The article traces Java’s concurrency evolution from heavyweight thread‑per‑request and complex reactive models to JDK 21’s virtual threads, which act as stack‑ful coroutines offering lightweight, heap‑allocated threads, full stack traces, and blocking‑I/O compatibility while preserving the familiar thread API.
This article examines the evolution of Java's concurrency model, starting from the classic "thread‑per‑request" model, moving through asynchronous/reactive programming, and finally introducing virtual threads as a stackful coroutine implementation in JDK 21.
In the traditional servlet ecosystem each request is handled by a dedicated OS thread, which leads to high memory consumption (e.g., a 1 MB stack per thread) and costly context switches. The article contrasts this with asynchronous/reactive models that avoid blocking kernel threads by using event‑loop mechanisms, but notes drawbacks such as complex callback chains and loss of thread‑local context.
The concepts of stackless (e.g., Kotlin) and stackful (e.g., Go) coroutines are explained. Stackless coroutines are implemented as state machines without a dedicated call stack, while stackful coroutines maintain an independent stack for each coroutine, enabling full stack traces and easier debugging.
Example of a stackless coroutine in Kotlin:
package org.example
import kotlinx.coroutines.*
suspend fun requestToken(): String {
println("当前requestToken执行线程为:${Thread.currentThread()}")
delay(1000) // simulate blocking
return "token"
}
suspend fun fetchData(token: String): String {
println("当前fetchData执行线程为:${Thread.currentThread()}")
delay(1000)
return "$token:success"
}
fun main() = runBlocking {
launch {
val token = requestToken()
val result = fetchData(token)
println(result)
}
println("当前main执行线程为:${Thread.currentThread()}")
delay(5000)
}The same logic rewritten with explicit callbacks shows how the suspend keyword is essentially syntactic sugar for a CPS transformation.
Example of a stackful coroutine in Go (goroutine):
package main
import (
"fmt"
"time"
)
func requestToken(tokenCh chan<- string) {
fmt.Println("requestToken")
time.Sleep(2 * time.Second)
tokenCh <- "token"
}
func fetchData(tokenCh <-chan string) {
token := <-tokenCh
fmt.Println("fetchData")
time.Sleep(2 * time.Second)
fmt.Printf("%s:success\n", token)
}
func main() {
tokenCh := make(chan string)
go requestToken(tokenCh)
go fetchData(tokenCh)
fmt.Println("主线程其他逻辑")
time.Sleep(5 * time.Second)
}Java virtual threads combine the advantages of stackful coroutines with the familiar thread API. They are lightweight, heap‑allocated user threads (n:m scheduling) that can block on I/O without blocking the carrier OS thread. Sample usage:
Thread.startVirtualThread(() -> {
System.out.println("hello virtual thread");
});
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
// business logic
System.out.println("task in virtual thread");
});Key characteristics of virtual threads include cheap creation/destruction, compatibility with existing thread‑local APIs, and the ability to run blocking I/O transparently. However, they can be "pinned" to a carrier thread in certain scenarios (critical sections, native calls, synchronized blocks), which may lead to resource waste. JEP 491 aims to eliminate pinning issues in a future JDK release.
In summary, virtual threads provide a stackful coroutine model that preserves the simplicity of synchronous code while delivering the scalability needed for high‑concurrency Java applications.
DaTaobao Tech
Official account of DaTaobao Technology
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.