Concurrency performance battle: Java 21 Virtual Threads vs. Go Threads vs. Elixir Erlang Threads

Balaji Arumugam
4 min readNov 9, 2023

--

I was testing how fast Java 21 Virtual Threads, Go Threads, and Elixir Erlang Threads can work. In this special test, I’m making a huge number of one million threads to see how well they can handle a simple printing job. Now, let’s break down the details of how they ran and what results we got for their performance.

don’t worry we will get it sorted!

1. Java 21 Virtual Threads

Java 21 introduces Virtual Threads, a new way of handling multiple tasks at once. These threads are designed to fix the issues of regular Java threads. They’re lightweight and work well for tasks like input/output operations. Plus, they come with advantages like using less memory, faster context switching, and simpler ways to manage multiple things happening at the same time.

Below is the Java 21 Virtual Threads experiment. Picture this: one million virtual threads each printing a simple message.

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class Java21VirtualThreadsDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
long startTime = System.currentTimeMillis(); // Record the start time in milliseconds
for (int i = 0; i < 1000000; i++) {
executor.execute(() -> System.out.println("Java 21 Virtual Thread #" + i));
}
executor.shutdown();
long elapsedTime = System.currentTimeMillis() - startTime;
System.out.println(
"Time taken to spawn and execute 1 million virtual threads: " + elapsedTime / 1000.0 + " seconds");
}
}

2. Go Threads

Now, let’s talk about Go, a language that introduces us to the trendy goroutines. These are lightweight, efficient, and really easy to handle. Goroutines are excellent for handling multiple tasks at once. Below is the Goroutines experiment.

package main

import (
"fmt"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup

start := time.Now() // Record the start time

for i := 0; i < 1000000; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println("Go Goroutine #", i)
}(i)
}

wg.Wait() // Wait for all goroutines to finish

elapsed := time.Since(start) // Calculate elapsed time

fmt.Printf("Time taken to spawn and execute 1 million goroutines: %s\n", elapsed)
}

3. Elixir Erlang Threads

Elixir, built on the Erlang virtual machine, takes a different route with its lightweight processes. Fault-tolerant and built for high availability, these processes are our contenders in the Elixir Erlang Threads experiment.

defmodule ElixirErlangThreadsDemo do
defmodule ProcessPrint do
def start_processes(n) do
start_time = :erlang.system_time(:millisecond) # Record the start time in milliseconds

tasks = Enum.map(1..n, fn i ->
Task.async(fn -> print_message(i) end) end
)

Enum.each(tasks, &Task.await(&1))

elapsed_time = :erlang.system_time(:millisecond) - start_time
IO.puts("Time taken to spawn and execute 1 million processes: #{elapsed_time} milliseconds")
end

defp print_message(i) do
IO.puts("Elixir Erlang Process ##{i}")
end
end

def run do
ProcessPrint.start_processes(100000)
end
end

ElixirErlangThreadsDemo.run()
let’s freaking go🚀🚀🚀🚀

Performance Showdown

Now, let’s crunch some numbers and analyze the performance of these thread superheroes.

Memory Usage:

- Java 21 Virtual Threads keep it light and breezy on the memory front.

- Go Threads (goroutines) are the minimalists of the group.

- Elixir Erlang Threads (processes) might carry a bit more baggage, but hey, it’s all for fault tolerance and robustness!

Scalability:

- Java 21 and Go Threads scale gracefully up to a million threads.

- Elixir Erlang Threads flex their muscles with superior scaling capabilities thanks to the BEAM VM’s process management.

Throughput:

- Java 21 and Go Threads handle I/O-bound tasks like champs.

- Elixir Erlang Threads are the legends when it comes to handling thousands of IO processes efficiently.

In this comparison, we’ve seen Java 21 Virtual Threads, Go Threads, and Elixir Erlang Threads showcase their unique strengths. Java 21 keeps it balanced, Go keeps it simple and efficient, and Elixir Erlang keeps it robust and fault tolerant.

Let’s see through the numbers:

Performance Metrics:

- To fork 1 million processes:

- Java 21 Virtual Threads took approximately ~7 seconds.

- Go Threads (goroutines) outpaced the competition at 4.5 seconds.

- Elixir Erlang Threads (processes) clocked in at 5.1 seconds.

Choosing your threading model? It’s all about trade-offs. You win some, you lose some.

It all boils down to your application’s needs — scalability, memory efficiency, and a dash of fault tolerance. Each model brings something special to the table, making them powerful tools for different landscapes. So, which thread tribe are you joining today? 😂

Yep, done for the post… 👋👋

--

--

Balaji Arumugam

Occasionally sharing insights and learnings. #software_engineering