Why Does Concurrency Have to Be So Hard in Java After 20 Years?

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5175

    #1

    Why Does Concurrency Have to Be So Hard in Java After 20 Years?

    Java has been around for nearly three decades, and we’ve had threads since day one. We got java.util.concurrent in 2004, lambdas in 2014, CompletableFuture improvements, reactive streams, and virtual threads in 2023…


    And yet, writing correct concurrent code in Java still feels like navigating a minefield.


    Why is this still so hard?




    The Problem: Too Many Half-Solutions

    Let’s take a simple case:


    Fetch data from three APIs concurrently, process results, handle errors, and respect a timeout.


    1. Threads (1995)





    public ListString> fetchData() {
    ListString> results = Collections.synchronizedList(new ArrayList());
    CountDownLatch latch = new CountDownLatch(3);

    Thread t1 = new Thread(() -> {
    try {
    results.add(callApi("api1"));
    } catch (Exception e) {
    // What do we do here?
    } finally {
    latch.countDown();
    }
    });

    // … repeat for t2, t3 …

    t1.start(); t2.start(); t3.start();

    try {
    latch.await(10, TimeUnit.SECONDS); // What if it times out?
    } catch (InterruptedException e) {
    // Now what? Cancel the threads?
    }

    return results; // Hope for the best
    }
    Problems:

    Manual lifecycle management

    No consistent error propagation

    Cancellation is basically impossible

    2. ExecutorService (2004)
    java
    Copy code
    public ListString> fetchData() throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(3);
    try {
    ListFutureString>> futures = List.of(
    executor.submit(() -> callApi("api1")),
    executor.submit(() -> callApi("api2")),
    executor.submit(() -> callApi("api3"))
    );

    ListString> results = new ArrayList();
    for (FutureString> f : futures) {
    try {
    results.add(f.get(10, TimeUnit.SECONDS));
    } catch (TimeoutException e) {
    f.cancel(true);
    }
    }
    return results;
    } finally {
    executor.shutdown();
    executor.awaitTermination(5, TimeUnit.SECONDS);
    executor.shutdownNow(); // Fingers crossed
    }
    }
    Better, but still verbose and error-prone.

    3. CompletableFuture (2014)
    java
    Copy code
    public CompletableFutureListString>> fetchData() {
    var f1 = CompletableFuture.supplyAsync(() -> callApi("api1"));
    var f2 = CompletableFuture.supplyAsync(() -> callApi("api2"));
    var f3 = CompletableFuture.supplyAsync(() -> callApi("api3"));

    return CompletableFuture.allOf(f1, f2, f3)
    .thenApply(v -> List.of(f1.join(), f2.join(), f3.join()))
    .orTimeout(10, TimeUnit.SECONDS)
    .exceptionally(ex -> List.of());
    }
    Cleaner, but:

    No structured concurrency

    Cancellation is awkward

    Error handling is ad-hoc

    4. Virtual Threads (2023)
    java
    Copy code
    public ListString> fetchData() throws Exception {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var futures = List.of(
    executor.submit(() -> callApi("api1")),
    executor.submit(() -> callApi("api2")),
    executor.submit(() -> callApi("api3"))
    );

    return futures.stream()
    .map(f -> {
    try {
    return f.get(10, TimeUnit.SECONDS);
    } catch (Exception e) {
    throw new RuntimeException(e);
    }
    })
    .toList();
    }
    }
    Virtual threads help performance, but the core problems remain:

    No automatic cancellation

    No clear error boundaries

    Timeouts are still manual

    # What’s Wrong Here?
    After 20+ years, Java’s concurrency is still missing:

    Structured Concurrency — no parent/child lifetimes, leading to leaks.

    Reliable Cancellation — interruption is unreliable and inconsistent.

    Consistent Error Handling — failures don’t cleanly propagate.

    Resource Safety — Executors and threads must be closed manually.

    Context — no standard way to pass cancellation tokens, timeouts, or tracing.

    Other languages got this right:

    Go has context.WithTimeout for group cancellation.

    Kotlin has coroutines with structured scopes.

    C# has Tasks with CancellationToken.

    # What We Actually Want








    try (var scope = new CoroutineScope()) {

    var results = List.of("api1", "api2", "api3").stream()

    .map(api -> scope.async(suspend -> callApi(suspend, api)))

    .map(handle -> handle.join())

    .toList();

    return results;

    } // Automatic cleanup, cancellation, and error propagation








    This is:

    Structured — parent/child relationships are explicit

    Cancellable — cooperative and consistent

    Safe — resources cleaned up automatically

    Transparent — errors bubble naturally

    # Enter JCoroutines 🚀
    Concurrency should feel like an elevator: press a few clear buttons and trust the machinery.

    That’s what I set out to build with JCoroutines:

    Structured concurrency by default

    Explicit context passing (cancellation, timeouts, schedulers)

    No compiler magic — just clean Java APIs on top of virtual threads

    It’s small, explicit, and available today.

    Try It Out
    On Maven Central:

    maven xml

    tech.robd
    jcoroutines
    0.1.0

    Or Gradle:

    kotlin gradle.build.kts

    implementation("tech.robd:jcoroutines:0.1.0")

    The Path Forward
    Java itself is heading this way (see JEP 428 on structured concurrency), but it will take years before that’s fully stable.

    Meanwhile, JCoroutines gives you these patterns now — using just Java 21+ and virtual threads.

    [📦 Maven Central](https://central.sonatype.com/artifac...bd/jcoroutines)

    [💻 GitHub repo](https://github.com/robdeas/jcoroutines)









    More...
Working...