Core Java

Kotlin Coroutines vs Virtual Threads: Two Answers to Cheap Concurrency

Every few months, someone on a team asks the same question: now that Java has virtual threads, do we still need Kotlin coroutines? The honest answer is that both solve the same pain, yet they solve it in very different places.

The Shared Problem

First, let’s name the pain. A classic Java thread is a thin wrapper around an operating system thread, so the number you can run is limited. JEP 444 says this directly. As a result, servers that wait on databases and remote calls waste most of their threads doing nothing.

Both technologies attack this waste. However, one does it at compile time, while the other does it at runtime.

Answer One: Coroutines, a Compiler Trick

Kotlin coroutines are mostly a language and library feature. The compiler rewrites every suspend function into a state machine, so a function can pause and later resume without holding a thread. The design is described in the paper Kotlin Coroutines: Design and Implementation by Elizarov and colleagues (Onward! 2021).

Consequently, coroutines are stackless, and the pause points are visible in the function signature. That visibility has a price, since the suspend keyword spreads through your call chain. On the other hand, it brings real benefits:

  • Structured concurrency is built in, so cancelling a parent cancels its children.
  • The same code runs on the JVM, Android, JavaScript and Native.
  • Dispatchers let you choose where work runs.

Notably, the default Dispatchers.IO is a shared pool. For blocking calls it allows 64 threads, or the number of cores if that is larger.

Answer Two: Virtual Threads, a Runtime Trick

Virtual threads take the opposite route. Instead of changing how you write code, the JDK changes what a thread is. They arrived as a preview in JDK 19, a second preview in JDK 20, and became final in JDK 21 through JEP 444. Your code stays plain and blocking, but when it waits, the JDK unmounts the virtual thread and frees the underlying platform thread.

Early on, there was one big catch. Virtual threads were pinned to their carrier while inside synchronized blocks. Fortunately, JEP 491 removed nearly all of those cases in JDK 24. Native frames can still pin, so it is not a complete fix. For a practical summary, see InfoQ’s Virtual Threads After JDK 24.

Side by side

AspectKotlin coroutinesVirtual threads
Where it livesCompiler plus kotlinx.coroutinesThe JDK runtime
Code styleExplicit suspend functionsOrdinary blocking code
SchedulingYour choice of dispatcherThe JDK’s own scheduler
CancellationHierarchical, built inInterrupts; structured API still evolving
PlatformsJVM, Android, JS, NativeJVM only, JDK 21 or newer

What the Numbers Say

Real comparisons are rare, so let’s look at one peer-reviewed attempt. A team at Lublin University of Technology tested coroutines, threads, a thread pool and virtual threads on I/O work, using JMH, Kotlin 2.0.0 and JDK 22.0.1 on a Ryzen 5 5600X. First, here is memory use during 5,000 file read and write operations.

Data: Grabowiec, Wiktor and Smołka, JCSI 33 (2024), Table 2. One machine, one scenario. The paper’s text gives 1165 MB for virtual threads, but its table says 1162 MB; the table value is plotted.

Next, the REST API scenario shows time per operation for coroutines on the IO dispatcher against a fixed thread pool.

Data: same paper, section 6, in ms/op. The authors report plain threads were 2.6 times slower than IO-dispatcher coroutines at 500 iterations and 4.3 times at 5,000. Virtual threads were not tested here because of an HttpClient limitation, as the authors note.

How to read this fairly

Be careful, though. This is one study, on Windows 11, and its file test is not network I/O. Moreover, JDK 22 predates the JEP 491 fix. Also, Dispatchers.IO capped these coroutines at 64 threads, so bounded parallelism probably explains part of the result, rather than suspension alone. Therefore, treat the charts as a starting point and measure your own workload.

Choosing Between Them

In practice, the choice follows your codebase more than any benchmark. If you share code across Android, iOS and the backend, coroutines are the only option. Likewise, if you rely on Flow or hierarchical cancellation, they fit naturally. In contrast, a Java service full of blocking libraries such as JDBC gains the most from virtual threads, because nothing needs rewriting. Debugging also feels familiar, since virtual threads are real Thread objects.

You do not have to pick only one. Kotlin can wrap any Java executor as a dispatcher, which includes the virtual-thread-per-task executor. As a result, many teams keep coroutines for structure and let virtual threads absorb blocking calls.

Quick decision checklist

  • Need Kotlin Multiplatform or Android? Choose coroutines.
  • Mostly Java with blocking libraries? Choose virtual threads on JDK 24 or newer.
  • Want parent-child cancellation today? Choose coroutines.
  • Unsure about performance? Benchmark your own workload first.

What We Have Learned

Coroutines and virtual threads both make waiting cheap, yet they live at different layers. Coroutines are a compiler transformation with explicit suspension points, built-in structure and multiplatform reach. Virtual threads are a runtime feature that keeps blocking code simple and works with existing Java libraries. The one study we examined favoured bounded pools and coroutines on its workloads, but its scope was narrow and it predates the JDK 24 pinning fix. So the best choice depends on your platform and your code, and combining both is a perfectly valid answer.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button