~/blog/HFT-On-JDK25
Published on

Exploring Java JDK 25: Virtual Threads, Structured Concurrency, and Performance

3408 words18 min read
Authors
  • avatar
    Name
    Rehber Moin
    LinkedIn
    @r0m

HFT on Java? Java is Dead. Long Live Java.

Ryan Gosling is Literally Me

Hello and welcome to my not-so-original blog on JDK 25. Apologies for the clickbait-y title — it’s more click than bait, and probably has nothing to do with your day job. Let’s move past that and get to the meat of the matter.

It’s like clockwork (just like iPhone releases) that these new JDK versions roll out. Easy to shrug off, ignore, and continue living your life pretending your code doesn’t need another runtime upgrade. Right? Maybe you’re right — but hold on just a second.

I’m not here to sell you a shiny new Java that will erase all your tech debt, make your legacy systems behave, or give you an instant 10x performance improvement that magically promotes you to Staff Software Engineer. No, no, no.

What it does do — and here’s where we sneak in a little HFT swagger — is give Java the tools to finally behave like it might actually handle thousands of concurrent operations without melting your CPU or making you cry in meetings. Virtual threads, structured concurrency, and all the syntactic candy in JDK 25 make highly concurrent, low-latency workloads feel… possible.

It’s not magic. It won’t save your project from spaghetti code. But it does make certain things — concurrency, boilerplate, and patterns — less painful, more intuitive, and dare I say… slightly fun again, even if you’re not building the next Wall Street HFT engine.

Of all the new feature that have been rolled out (still a lot in preview), there are a few notable changes that are introduced as a part of JDK25 that really make it worth taking a look. Not for the sake of James Gosling (or Ryan [Literally Me]), but for the sake of advancing the evergreen 3-billion device supported language that is our beloved Java.

Ryan Gosling is Literally Me

Now, I know what you’re thinking —

Oh great, another Java release with more JEP numbers than features I’ll ever use.

And honestly? You’re not wrong. There’s so much happening in JDK 25 that even James Gosling would probably squint and go,

Wait… we did what now?

So before you ask — no, I won’t be covering everything. If I did, this blog would be longer than your project’s pom.xml.

Ryan Gosling is Literally Me

Here’s a quick roll call of the features I won’t dive into — not because they’re bad, but because they either belong in a PhD thesis or an AI marketing brochure.

The “AI-Ready” (™️) Features

All marketed as “AI-friendly,” which basically means “We optimized it and sprinkled in a buzzword.”

The “Enterprise Stuff You’ll Google Later”

And the “You’ll Thank It Later” Category

I know, I know — it’s a lot. But instead of drowning you in every JEP like it’s a game of “Buzzword Bingo,” I’m going to walk you through the ones that actually change how you write Java — the ones that make the language feel fresh, modern, and (dare I say it)… fun again.

You’re probably thinking:

Cool looking list Rehber. Can we go back to Glazing how cool the new Bun Version is or how fast this new Go Service is?

Well, hold your horses — this is where Java finally shows it can actually be fun, readable, and modern. Lets take a slightly closer look and see these features in just a little more detail. (Not all cause I'm employed and have a life).


Compact Java: Slimmer Syntax, Slimmer Objects

Remember when writing “Hello, World!” in Java felt like filing your taxes?

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

That’s 5 lines, one class, one method, two braces, and exactly one line of useful code.

Enter Compact Source Files (JEP 512)

Java 25 finally lets us write small scripts without the ceremony.

You can now skip the public class nonsense and jump straight into the code.

Old & Heavy

public class Demo {
    public static void main(String[] args) {
        System.out.println("Java was verbose, but not anymore!");
    }
}

New & Zen

void main() {
    System.out.println("Java was verbose, but not anymore!");
}
  • ✅ No class declaration
  • ✅ Works directly with javac and java
  • ✅ Supports instance main methods — yes, you can access this inside your main!

Instance Main Example

String name = "Alice";

void main() {
    greet();
}

void greet() {
    System.out.println("Hello, " + name + " 👋");
}

Run it directly with:

java MyCompactFile.java

Boom — no boilerplate, no public static void main(String[] args). Just pure, clean Java. Like it’s been hitting the syntax gym. 🏋️‍♀️

Behind the Scenes: Compact Object Headers (JEP 519)

Java didn’t just slim down your code — it slimmed down your objects too.

Every Java object carries a bit of metadata (called an object header). Before JDK 25, this header was a 16-byte backpack every object carried around. Now? It’s been put on a diet.

FeatureBefore (JDK 24)Now (JDK 25)
Header Size16 bytes~8 bytes
Memory UsageHigherLower
Cache EfficiencyMehMuch Better
PerformanceDecentFaster under load

That means:

  • Smaller memory footprint
  • Faster object allocation
  • Better cache locality (because your data fits better in CPU caches)

Perfect for microservices, data-heavy workloads, and your RAM-starved laptop.

Quick Demo

record User(String name, int age) {}

void main() {
    long before = Runtime.getRuntime().freeMemory();
    var users = new User[1_000_000];
    for (int i = 0; i < users.length; i++)
        users[i] = new User("User" + i, i);
    long after = Runtime.getRuntime().freeMemory();

    System.out.printf("Approx memory used: %d bytes%n", (before - after));
}

Run this on JDK 24 vs JDK 25 — You’ll see less memory used and better allocation times thanks to compact headers.

Java 25: now 50% fewer braces, 30% fewer bytes, and 100% more fun.


Virtual Threads & Structured Concurrency: Java Finally Learns to Adult 🧵☕

Ryan Gosling is Literally Me

If you’ve ever tried running multiple tasks in classic Java, you know the pain: manually managing thread pools, futures, and exception handling, all while silently praying your CPU doesn’t explode.

JDK 25 introduces Virtual Threads and Structured Concurrency, effectively saying:

Stop juggling threads like flaming swords. Let me organize them for you.

Virtual Threads: Concurrency Without Losing Your Mind 🧘‍♂️

If traditional Java threads were sumo wrestlers, virtual threads are yoga instructors: light, flexible, and able to handle hundreds without breaking a sweat.

Classic Java threads:

ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> doSomeIO());
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
  • ✅ Works
  • ❌ Eats RAM like a hungry sumo
  • ❌ Creates thread dumps that look like spaghetti
  • ❌ Makes your CPU scream for mercy

Virtual Threads (JDK 25):

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 1; i <= 1000; i++) {
        int taskNum = i;
        executor.submit(() -> System.out.println("Task " + taskNum + " executed by " + Thread.currentThread()));
    }
}
  • ✅ 1000 tasks, 1000 virtual threads
  • ✅ Tiny memory footprint
  • ✅ Clean, readable code

Why it’s revolutionary:

  1. Massive scalability – thousands of threads effortlessly
  2. Simplified code – no more custom thread pools
  3. Automatic scheduling – JVM handles all the juggling
  4. Perfect pairing with Structured Concurrency for complex workflows
FeatureClassic ThreadsVirtual Threads
Thread CostHigh (OS thread)Low (lightweight JVM-managed)
Number of ThreadsTens to hundredsThousands to tens of thousands
I/O BlockingExpensiveCheap, non-blocking under the hood
BoilerplateHighMinimal
Error ManagementManualIntegrates perfectly with structured concurrency

Structured Concurrency: Stop the Chaos

Structured concurrency treats all tasks within a scope as a single logical unit:

  • If one task fails, the scope handles it gracefully
  • If all succeed, you get all results easily
  • Combine with virtual threads to run thousands of tasks without breaking a sweat

Classic Java chaos:

ExecutorService executor = Executors.newFixedThreadPool(3);

Future<String> t1 = executor.submit(() -> doTransaction());
Future<String> t2 = executor.submit(() -> sendNotification());
Future<String> t3 = executor.submit(() -> logTransaction());

try {
    System.out.println(t1.get());
    System.out.println(t2.get());
    System.out.println(t3.get());
} catch (Exception e) {
    e.printStackTrace();
} finally {
    executor.shutdown();
}
  • ✅ Works
  • ❌ Boilerplate overload
  • ❌ Error handling nightmare
  • ❌ Hard to scale

Structured Concurrency + Virtual Threads:

try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
    var t1 = scope.fork(() -> doTransaction());
    var t2 = scope.fork(() -> sendNotification());
    var t3 = scope.fork(() -> logTransaction());

    scope.join(); // wait for all tasks
    System.out.println(t1.get());
    System.out.println(t2.get());
    System.out.println(t3.get());
}
  • ✅ No explicit executor
  • ✅ Automatic error handling
  • ✅ Clean, readable, maintainable
  • ✅ Scales effortlessly with virtual threads

What’s Cool:

  • Fork once, join once – no juggling Futures
  • Tasks fail together if one fails
  • Perfect for highly concurrent, I/O-heavy workloads
  • Your coworkers might actually understand your code (miracle!)

Compare & Contrast

FeatureOld WayStructured Concurrency
ThreadsManual executor + futuresFork within scope, virtual threads
Error handlingManual try/catchAutomatic: fails all if one fails
BoilerplateVery highMinimal
ScalabilityModerate, expensiveExtremely high (virtual threads)
ReadabilityLowHigh

Run the Full Demo Yourself

I’ve prepared a full banking simulation demo with:

  • Account creation
  • Random transactions
  • Notifications
  • Logging
  • Structured concurrency handling

🔗 Check out the full script on Gist: Structured Concurrency Demo

Benchmarking Results

TL;DR: Virtual threads + structured concurrency = Java finally behaving like an adult. Flexible, lightweight, readable, and capable of handling thousands of concurrent tasks with HFT-like efficiency, without making your CPU cry or forcing you to trade your sanity for performance.


Scoped Values: Goodbye ThreadLocal, Hello Sanity

If you’ve ever used ThreadLocal, you know it’s like letting each thread have its own secret diary… that nobody ever remembers to close. It’s powerful — and also the source of some of the most mysterious memory leaks in enterprise history.

So Java 25 finally said:

Maybe giving threads global mutable state wasn’t the best idea. 😅

And thus came Scoped Values — a cleaner, safer, immutable alternative to ThreadLocal.

The Old Way: ThreadLocal Shenanigans

private static final ThreadLocal<String> USER = new ThreadLocal<>();

void handleRequest(String username) {
    USER.set(username);
    try {
        doSomething();
    } finally {
        USER.remove(); // pray you never forget this
    }
}

void doSomething() {
    System.out.println("User: " + USER.get());
}
  • ✅ Works.
  • ❌ Mutable.
  • ❌ Easy to forget cleanup.
  • ❌ Nightmare with virtual threads.

The New Way: Scoped Values (JEP 506)

Scoped values are immutable, safe, and automatically managed by the JVM — no manual cleanup, no thread leaks, no weird cross-thread contamination.

import java.lang.ScopedValue;

public class ScopedValueDemo {

    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) throws Exception {
        ScopedValue.where(USER, "Alice").run(() -> {
            System.out.println("In scope: " + USER.get());
            doSomething();
        });

        // Out of scope: accessing USER now throws an exception
        try {
            System.out.println(USER.get());
        } catch (IllegalStateException e) {
            System.out.println("Access outside scope: " + e.getMessage());
        }
    }

    static void doSomething() {
        System.out.println("Nested access: " + USER.get());
    }
}
  • ✅ Immutable and safe.
  • ✅ Automatically cleaned when scope ends.
  • ✅ Works perfectly with virtual threads (each gets its own isolated copy).
  • ✅ No need for .remove() calls.

Scoped Values + Virtual Threads = Harmony

Here’s where the magic happens — share contextual, immutable data with a bunch of virtual threads safely:

import java.lang.ScopedValue;
import java.util.concurrent.Executors;

public class ScopedValueVirtualDemo {

    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            ScopedValue.where(REQUEST_ID, "REQ-12345").run(() -> {
                for (int i = 1; i <= 5; i++) {
                    int taskId = i;
                    executor.submit(() ->
                        System.out.println("Task " + taskId + " - " + REQUEST_ID.get())
                    );
                }
            });
        }
    }
}

Each virtual thread safely reads the same REQUEST_ID, with zero shared-state weirdness.

Quick Summary

FeatureThreadLocalScopedValue
MutabilityMutableImmutable
CleanupManualAutomatic
Thread SafetyEasy to misuseGuaranteed safe
Works with Virtual Threads❌ Bug-prone✅ Perfect
Introduced InJava 1.2 (ouch)Java 25

Primitive Patterns & Smarter Modules: Java Finally Cleans Its Room

Java’s growing up — fewer casts, fewer config files, fewer headaches. Two underrated stars in JDK 25 are Primitive Patterns (JEP 507) and Module Import Declarations (JEP 511) — both here to declutter your life.

Primitive Patterns (JEP 507)

Before:

Object value = 42;
if (value instanceof Integer) {
    int n = ((Integer) value).intValue();
    System.out.println(n * 2);
}

Now:

Object value = 42;
if (value instanceof int n) {
    System.out.println(n * 2);
}
  • ✅ No casting
  • ✅ Works in switch too
  • ✅ Cleaner and safer

Example:

Object x = 3.14;
String result = switch (x) {
    case int i -> "int " + i;
    case double d -> "double " + d;
    default -> "something else";
};

Java finally patterns on primitives like a grown-up.

Module Import Declarations (JEP 511)

Old way:

module com.smartbank.app {
    requires com.smartbank.services;
}

New way:

import module com.smartbank.services;

void main() {
    System.out.println("Modular and minimal!");
}
  • ✅ One consistent import syntax
  • ✅ Easier to read
  • ✅ No more module-info.java headaches

Wrapping Up: Java 25 — Still Java, Just Slightly Cooler

Ryan Gosling is Literally Me

So, here we are at the end of our little JDK 25 joyride.

  • Virtual threads? ✅
  • Structured concurrency? ✅
  • Compact source files, primitive patterns, and all those other JEPs that sound like IKEA instructions? ✅

Does this mean your life as a Java developer will instantly become blissful? Will all your legacy code start behaving like well-trained puppies? Will that one microservice finally stop being slower than a snail on vacation?

Not entirely. 😅

JDK 25 is not magic. It won’t automatically untangle your spaghetti code or make your coworkers stop committing questionable PRs. What it does do is bring actual, tangible improvements: faster and more scalable concurrency, cleaner syntax, and fewer boilerplate headaches. It makes Java more intuitive, less painful, and dare I say… fun again.

So don’t rush to upgrade just because the hype screams “new JDK = new life.” Play with the shiny toys. Benchmark the performance gains. Experiment with virtual threads and structured concurrency. And maybe, just maybe, enjoy a rare moment where Java feels modern, snappy, and actually enjoyable.

TL;DR: JDK 25 is great, not magic—but your code might run faster, your syntax will look cleaner, and your future self may just thank you.