Java concurrency
A Claude Code plugin and marketplace providing a skills library for high-quality Java development.
npx -y skills add mtkhawaja/java-skills --skill java-concurrencyAssembled 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 commonForkJoinPool(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 callingget()/join(). A single lazy pipelinestream().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 (
ExecutorServiceisAutoCloseablesince Java 19 —close()awaits/cancels tasks). For a shared executor, callshutdown()+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
synchronizedblock pins the carrier thread (defeats virtual threads). Use aReentrantLockaround blocking sections instead ofsynchronized. - Don't block the common
ForkJoinPool(parallel streams,CompletableFuture.*Asyncwithout 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
volatilefor visibility of simple flags; never rely on un-synchronized reads of mutable state. ThreadLocal(and MDC) on pooled/carrier threads must be cleared infinallyto avoid leakage — seejava-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
| Rationalization | Reality |
|---|---|
"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 workstream().map(...submit...).map(...get/join...)in one lazy pipeline (sequential)- Submitting one task per element with no
Semaphore/bound - An
ExecutorServicecreated and never closed/shut down - A blocking call inside
synchronizedon a virtual-thread path - Shared mutable field read across threads without
volatile/lock