Skip to content

Java 19

Neelu edited this page Jul 26, 2026 · 1 revision

☕ Java 19: Virtual Threads Preview (Project Loom Begins)

Release Date: September 20, 2022 | Type: Standard (non-LTS) | Status: ⚠️ End of Life


📋 Table of Contents

  1. Overview
  2. Core Features
  3. Best Practices

🎯 Overview

Java 19 previews the most significant concurrency change in Java's history: Virtual Threads (Project Loom). It also previews Record Patterns and Structured Concurrency — three features that together redefine how Java handles concurrent I/O-bound workloads.

flowchart TD
    A["Platform Threads\n1:1 mapped to OS threads\nExpensive: ~1MB stack each\nThousands = resource exhaustion"] -->|Project Loom| B["Virtual Threads\nM:N mapped to OS carrier threads\nCheap: ~few KB each\nMillions possible"]
Loading

🚀 Core Features

1️⃣ Virtual Threads (Preview)

What: Lightweight threads managed by the JVM (not the OS), enabling massive concurrency for I/O-bound code without the complexity of reactive/async programming.

// Traditional platform thread — expensive, OS-managed
Thread.ofPlatform().start(() -> {
    System.out.println("Platform thread: " + Thread.currentThread());
});

// Virtual thread — cheap, JVM-managed (Preview in 19, standard in 21)
Thread.ofVirtual().start(() -> {
    System.out.println("Virtual thread: " + Thread.currentThread());
});

// The killer use case: one virtual thread per request/task, written as
// simple BLOCKING code — no callbacks, no reactive operators
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        int taskId = i;
        executor.submit(() -> {
            // Blocking I/O call — perfectly fine on a virtual thread!
            String result = callSlowExternalApi(taskId);
            System.out.println(result);
        });
    }
} // executor.close() waits for all tasks to complete
sequenceDiagram
    participant App
    participant VT as Virtual Thread
    participant CT as Carrier (OS) Thread
    participant IO as Blocking I/O

    App->>VT: submit(task)
    VT->>CT: mounted on carrier thread
    VT->>IO: blocking call (e.g., HTTP request)
    Note over VT,CT: VT unmounts from carrier —<br/>carrier thread is FREED to run other virtual threads
    IO-->>VT: response ready
    VT->>CT: remounts on any available carrier
    VT-->>App: task complete
Loading

100,000 platform threads would crash most systems. 100,000 virtual threads run comfortably because they don't each consume a full OS thread — they're multiplexed onto a small pool of "carrier" platform threads, and unmount from the carrier whenever they block on I/O.


2️⃣ Record Patterns (Preview)

What: Destructure record values directly in instanceof and (later) switch — extract fields without calling accessor methods manually.

record Point(int x, int y) {}
record Line(Point start, Point end) {}

// Before: manual accessor calls
if (obj instanceof Point p) {
    int x = p.x();
    int y = p.y();
}

// Record patterns (Preview) — destructure directly
if (obj instanceof Point(int x, int y)) {
    System.out.println("x=" + x + ", y=" + y);
}

// Nested destructuring — this is where it really shines
if (shape instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
    double length = Math.hypot(x2 - x1, y2 - y1);
    System.out.println("Line length: " + length);
}
flowchart LR
    A["Line(Point(x1,y1), Point(x2,y2))"] -->|record pattern| B["Destructures in\none expression"]
    B --> C["x1, y1, x2, y2\ndirectly usable"]
Loading

3️⃣ Structured Concurrency (Incubator)

What: Treat a group of related concurrent subtasks as a single unit of work — if one fails, the others are cancelled automatically. No more "fire and forget" threads that leak or outlive their purpose.

// Before: manually coordinating two parallel calls with ExecutorService/Future
// is error-prone — a failure in one doesn't cancel the other

// Structured Concurrency (Incubator)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> userFuture = scope.fork(() -> fetchUser(userId));
    Future<Order> orderFuture = scope.fork(() -> fetchOrder(orderId));

    scope.join();           // wait for both
    scope.throwIfFailed();  // propagate failure from either

    User user = userFuture.resultNow();
    Order order = orderFuture.resultNow();
    return new Response(user, order);
}
// If fetchOrder() throws, fetchUser() is automatically cancelled —
// no orphaned threads, no resource leaks
flowchart TD
    A[Parent scope opens] --> B[fork: fetchUser]
    A --> C[fork: fetchOrder]
    B --> D{join}
    C --> D
    D -->|Any task fails| E["Cancel all sibling tasks\nautomatically"]
    D -->|All succeed| F[Continue with results]
Loading

4️⃣ Pattern Matching for switch (3rd Preview)

static String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0 -> "negative int";       // guarded pattern
        case Integer i -> "non-negative int: " + i;
        case String s when s.isBlank() -> "blank string";
        case String s -> "string: " + s;
        case null -> "it's null";
        default -> "something else";
    };
}

💡 Best Practices

✅ Start experimenting with virtual threads in side projects — for I/O-heavy services (REST APIs calling databases/other APIs), this eventually replaces the need for reactive frameworks in many cases
✅ Use structured concurrency for any "fan-out, fan-in" pattern where sibling tasks should live and die together
❌ Don't use virtual threads for CPU-bound work — they only help when threads are blocked on I/O; a CPU-bound virtual thread just occupies its carrier thread the same as a platform thread would
❌ Don't rely on preview APIs (--enable-preview) in production Java 19 code — the API shapes changed before standardization in 21


Clone this wiki locally