agentsclimarketplace

Java concurrency

Skill mtkhawaja/java-skills/skills/java-concurrency

A Claude Code plugin and marketplace providing a skills library for high-quality Java development.

Install
npx -y skills add mtkhawaja/java-skills --skill java-concurrency

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when writing or reviewing concurrent/parallel Java — fanning out blocking calls, thread pools/executors, virtual threads, parallel streams, CompletableFuture, or shared mutable state. Covers choosing the right executor, submitting all tasks before joining, bounding concurrency, executor lifecycle, virtual-thread pinning, and visibility. Catches parallelStream misuse, lazy-stream sequential fan-out, unbounded fan-out, and leaked executors.

SKILL.md

6.3 KB, as published. Nobody here has run it

Java Concurrency

Overview

Make concurrency explicit, bounded, and testable, and avoid shared mutable state. The two recurring bugs: fan-out that is accidentally sequential, and fan-out that is unbounded.

Choosing the tool

  • Blocking / I/O-bound work → virtual threads (Java 21+): Executors.newVirtualThreadPerTaskExecutor(). Cheap, one per task, releases the carrier while blocked.
  • CPU-bound work → a bounded pool sized near the core count, or a parallel stream (CPU-bound, side-effect-free, large data only).
  • Never parallelStream() for I/O-bound or side-effecting work. It runs on the shared common ForkJoinPool (sized to cores), so it gives almost no parallelism for blocking calls and starves the rest of the app that shares that pool.

Fan out correctly

  • Submit all tasks first, then join. Materialize the futures (.map(submit).toList()) before calling get()/join(). A single lazy pipeline stream().map(submit).map(get) runs sequentially — each element is joined before the next is even submitted.
  • Bound the concurrency. Don't fire thousands of simultaneous calls at a downstream — cap in-flight work with a Semaphore (or a sized executor), or it becomes a self-inflicted DoS.
private static final int MAX_IN_FLIGHT = 50;

public List<Enriched> enrichAll(final List<Item> items) {
    final var limiter = new Semaphore(MAX_IN_FLIGHT);
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {   // closed = waits for tasks
        final List<Future<Enriched>> futures = items.stream()
            .map(item -> executor.submit(() -> {
                limiter.acquire();
                try { return enrichmentClient.enrich(item); }            // blocking I/O
                finally { limiter.release(); }
            }))
            .toList();                                                   // submit ALL before joining
        return futures.stream().map(this::join).toList();
    }
}

private Enriched join(final Future<Enriched> f) {
    try {
        return f.get();
    } catch (final ExecutionException e) {
        throw new EnrichmentException("enrichment failed", e.getCause()); // preserve cause
    } catch (final InterruptedException e) {
        Thread.currentThread().interrupt();                              // restore the flag
        throw new EnrichmentException("interrupted", e);
    }
}

Executor lifecycle

  • Always shut executors down. For call-scoped work use try-with-resources (ExecutorService is AutoCloseable since Java 19 — close() awaits/cancels tasks). For a shared executor, call shutdown() + awaitTermination(...) on application stop.
  • Don't create a new pool per call for hot paths; share a bounded one (virtual-thread executors are cheap, but still close them).

Virtual-thread pitfalls

  • Don't pool virtual threads — use the per-task executor; don't wrap them in a fixed pool.
  • Pinning: blocking inside a synchronized block pins the carrier thread (defeats virtual threads). Use a ReentrantLock around blocking sections instead of synchronized.
  • Don't block the common ForkJoinPool (parallel streams, CompletableFuture.*Async without an explicit executor) with I/O.

Shared state & visibility

  • Prefer immutability and thread confinement. For genuinely shared mutable state use concurrent collections (ConcurrentHashMap), atomics (AtomicLong), or explicit locks — not bare fields.
  • Use volatile for visibility of simple flags; never rely on un-synchronized reads of mutable state.
  • ThreadLocal (and MDC) on pooled/carrier threads must be cleared in finally to avoid leakage — see java-observability.

Structured concurrency (preview)

StructuredTaskScope is the clean "fork several, fail fast, auto-cancel siblings" tool, but it is preview through Java 25 — only use it if the project enables --enable-preview (see the version-gating rules in java-development). Otherwise use the bounded executor + join pattern above.

Common mistakes

RationalizationReality
"parallelStream() makes it concurrent"It's the shared common pool, sized to cores — useless for blocking I/O and starves the app. Use a virtual-thread executor.
"stream().map(submit).map(get) runs in parallel"Lazy streams join each element before submitting the next → sequential. Materialize futures with .toList() first.
"Fire a task per item, they're cheap"Unbounded fan-out hammers the downstream. Cap in-flight work with a Semaphore.
"Virtual threads don't need pools, so no cleanup"Still close the executor (try-with-resources).
"synchronized is fine around the remote call"It pins the carrier thread under virtual threads — use ReentrantLock.
"One thread writes, one reads a plain field"Without volatile/synchronization the read may never see the write.

Red flags — stop

  • parallelStream() over blocking/I/O or side-effecting work
  • stream().map(...submit...).map(...get/join...) in one lazy pipeline (sequential)
  • Submitting one task per element with no Semaphore/bound
  • An ExecutorService created and never closed/shut down
  • A blocking call inside synchronized on a virtual-thread path
  • Shared mutable field read across threads without volatile/lock

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.