From java-skills
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/java-skills:java-concurrencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Make concurrency **explicit, bounded, and testable**, and avoid shared mutable state. The two
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.
Executors.newVirtualThreadPerTaskExecutor().
Cheap, one per task, releases the carrier while blocked.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..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.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);
}
}
ExecutorService
is AutoCloseable since Java 19 — close() awaits/cancels tasks). For a shared executor, call
shutdown() + awaitTermination(...) on application stop.synchronized block pins the carrier thread (defeats virtual
threads). Use a ReentrantLock around blocking sections instead of synchronized.ForkJoinPool (parallel streams, CompletableFuture.*Async without an
explicit executor) with I/O.ConcurrentHashMap), atomics (AtomicLong), or explicit locks — not bare fields.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.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.
| 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. |
parallelStream() over blocking/I/O or side-effecting workstream().map(...submit...).map(...get/join...) in one lazy pipeline (sequential)Semaphore/boundExecutorService created and never closed/shut downsynchronized on a virtual-thread pathvolatile/locknpx claudepluginhub mtkhawaja/java-skills --plugin java-skillsProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.