Skip to Content
Course content

242: Comparing Green Threads to JVM Threads in Scala

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a high-end restaurant. In the "Platform Thread" model, you hire one dedicated waiter for every single table that walks through the door. That waiter does nothing but stand at that specific table. If the customers spend ten minutes staring at the menu, the waiter just stands there, staring back, unable to help anyone else. You quickly run out of floor space because you have 50 waiters standing around doing nothing, and your payroll is skyrocketing because you're paying for 50 full-time salaries just to handle 50 tables.

Now, imagine the "Green Thread" (or Virtual Thread) model. You hire one highly efficient waiter who can handle 50 tables. They take an order at Table 1, and the moment those customers start debating between the salmon or the steak, the waiter pivots to Table 2 to refill water. When Table 1 finally decides, the waiter zips back to them. You have the same number of customers being served, but you're only paying one salary and you aren't crowding the dining room.

Mapping the Waiter to the JVM

In Scala, when we talk about these two models, we're talking about how the underlying OS handles execution. Here is how that analogy maps to the technical reality:

  • The Dedicated Waiter is a Platform Thread. It's a wrapper around an OS thread. It's "heavy"—usually allocating about 1MB of stack memory. If you try to spawn 100,000 of these, your JVM will crash with an OutOfMemoryError long before you hit that number.
  • The Efficient Waiter is a Green Thread (known as Virtual Threads in Java 21+ or Fibers in libraries like ZIO). These are managed by the runtime, not the OS. They are "light," often taking up only a few hundred bytes.
  • Staring at the Menu is Blocking I/O. When your code waits for a database response or an API call, a platform thread stays "blocked," holding onto its 1MB of memory while doing absolutely nothing. A green thread "unmounts" from the CPU, letting another green thread run until the I/O response comes back.

The Cost of Switching Contexts

I've seen a lot of developers assume that since platform threads are "real," they must be faster. That's a trap. Every time the OS switches from one platform thread to another, it has to perform a "context switch." This involves saving registers and switching kernel stacks—it's expensive. It's like the waiter having to change their entire outfit and identity every time they move to a new table.

Green threads handle this in "user space." The Scala runtime (or the JVM) handles the swap, which is orders of magnitude cheaper. Let's look at how this looks in a modern Scala context using Java 21's Virtual Threads:

import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object ThreadComparison extends App {
  // The "Old" way: A fixed pool of platform threads
  // If we have 100 tasks that all block for 1 second, 
  // we can only handle 10 at a time.
  val platformExecutor = Executors.newFixedThreadPool(10)
  
  // The "New" way: Virtual Threads (Green Threads)
  // We can spawn 10,000 of these and the JVM won't even break a sweat.
  val virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()

  def simulateIoCall(id: Int): String = {
    Thread.sleep(1000) // Simulating a blocking network call
    s"Task $id complete"
  }

  // Using Virtual Threads to handle a massive burst
  (1 to 10000).foreach { i =>
    virtualExecutor.submit(() => {
      println(s"Processing $i on ${Thread.currentThread()}")
    })
  }
}

When Green Threads Aren't the Answer

I want to be clear: don't just replace every thread with a virtual one. Green threads are magic for I/O-bound work (waiting for networks, disks, or databases). However, if you are doing CPU-bound work—like calculating a massive prime number or processing a heavy image—green threads provide zero benefit. Why? Because the "waiter" isn't waiting for a menu; they are actually cooking the food. If the CPU is 100% busy, you can't "pivot" to another task to save time. In those cases, a traditional fixed thread pool tuned to your number of CPU cores is still the gold standard.




📋 Practical Task

Build a High-Concurrency I/O Stress Tester

Your goal is to create a Scala program that demonstrates the memory and performance difference between Platform Threads and Virtual Threads when handling blocking I/O.

Requirements:

  • Create a function simulateApiCall() that uses Thread.sleep(200) to simulate a network delay.
  • Implement a test harness that attempts to run 5,000 concurrent instances of this function.
  • First, attempt to run these 5,000 tasks using a newFixedThreadPool(100). Measure the total time taken to complete all tasks.
  • Second, run the same 5,000 tasks using Executors.newVirtualThreadPerTaskExecutor(). Measure the total time.
  • Print the time difference and the Thread.currentThread().isVirtual status (if using Java 21+) inside the task to verify which thread type is being used.

Expected Outcome: You should observe that the Virtual Thread executor completes the tasks significantly faster (nearly 200ms total) because it doesn't bottleneck at the pool size, whereas the Platform Thread pool will take much longer as it processes the 5,000 tasks in batches of 100.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.