Java 21 Virtual Threads - Back to Basics

Introduction

Java 21 LTS was recently released, and while it introduces various enhancements, there's one feature that particularly stands out, subtly reshaping the Java development landscape. While it hasn't made as pronounced a splash as Java 5's introduction of generics or Java 8's unveiling of lambdas and streams, this distinctive feature in Java 21 marks a significant turning point and commands our immediate notice.

That standout feature? Virtual threads. Their introduction to Java 21 challenges our decade-long reliance on asynchronous, non-blocking, and event-driven programming paradigms. Through this post, we will delve deeper into why Java 21's virtual threads might be signaling a shift back to the roots of synchronous programming. We'll unpack the advantages of this model and investigate whether the late transition from synchronous to asynchronous in Java was merely a brief detour from the idiomatic Java approach.

A Brief History of Asynchronous Programming

Over the last decade, the software industry has been enamored by the allure of asynchronicity. The idea was simple: since threads are expensive resources, why not create non-blocking models to achieve concurrency? This led to the rise of various programming paradigms, ranging from callback patterns to Promises and reactive programming, all attempting to make the best out of limited system resources.

However, as developers, it's important to recognize that these asynchronous patterns, while offering performance benefits, often diverge from our natural cognitive processes. Humans are inherently linear thinkers, and the synchronous model aligns better with our inherent way of reasoning about tasks and sequences. When we're forced to juggle multiple callbacks or chain promises, it introduces a mental overhead, making our code not only harder to write but also challenging to debug and maintain. In essence, while we gain in system performance, we often sacrifice developer sanity.

The problem, therefore, isn't just that asynchronicity introduces a level of complexity to our code, but that it also conflicts with our natural thought processes.

The Virtual Threads Revolution

Virtual threads mark the beginning of a fresh chapter in the Java narrative. This innovation stands out not just for its novelty, but for its underlying principle of simplification. Rather than merely adding to the programming landscape, virtual threads aim to eliminate some of the unwarranted complexity that developers have previously struggled with.

In essence, virtual threads make it possible for a high number (we're talking millions here) of threads to be executed concurrently without the associated overhead. With this comes a realization: Java was always designed with a thread-based synchronous programming model at its core. While tools like CompletableFuture and third-party solutions like Reactor Core (Flux/Mono) are invaluable in filling the gaps of asynchronicity, they are often misused due to their inherent complexity. This misuse often leads to hybrid application models, with some parts being synchronous and others asynchronous. Such a patchwork approach only complicates the development landscape, making the overarching context of applications harder to understand and reason about.


// Example code using virtual threads in Java 21
Thread.ofVirtual().start(() -> {
    // Your concurrent code here
});

 

By doing away with the performance bottleneck associated with traditional threads, virtual threads in Java 21 may very well obviate the need for complicated asynchronous programming models and bring Java back to its synchronous roots.

The JavaScript Evolution: A Case Study

It's worth noting how JavaScript has evolved over the years. Originally flawed by "callback hell", JavaScript evolved to introduce Promises and later async/await, essentially moving towards a more synchronous programming model. The general consensus? Few, if any, want to go back to the older ways.


// Using async/await in JavaScript
async function fetchData() {
    const data = await someAsyncOperation();
    return processData(data);
}

 

This shift underscores the superiority of a synchronous programming model versus the alternative.

The Orthogonality of Back-Pressure

Before we leave this, we need to highlight an important point about back-pressure: its orthogonality to the synchronous versus asynchronous dichotomy. Simply put, the concept of back-pressure—designed to prevent resource exhaustion by controlling data exchange rates—stands independent of whether a system operates synchronously or asynchronously.

Historically, back-pressure implementations have been more visible in asynchronous contexts, which have been the go-to in high-performance scenarios. But with breakthroughs like virtual threads in Java 21 and the goroutine approach in the Go language, synchronous models are making a strong comeback. As these synchronous paradigms gain traction and evolve in efficiency, it's only a matter of time before they too integrate back-pressure strategies. The essence of back-pressure, given its orthogonal nature, can seamlessly adapt to either paradigm—don't let anyone fool you into thinking otherwise.

Conclusion

Java 21, with its introduction of virtual threads, challenges us to rethink our obsession with asynchronous programming models. By solving the performance issue tied to threads, Java 21 may very well change the landscape of how we approach concurrency, encouraging us to revisit the simpler, more manageable, and arguably more cognitive-friendly synchronous model.

The next time you reach for an asynchronous framework, pause and consider whether that's the best path. With virtual threads, Java 21 has given us a compelling alternative that warrants our attention.

By embracing these changes, we might find that the best way to handle concurrency is, paradoxically, to go back to basics. Just as in JavaScript, sometimes the most profound advances are the ones that simplify, rather than complicate, our code.

Virtual Threads Documentation

Back to blog