| name | java-concurrency-and-virtual-thread |
| description | Use this skill when statically reviewing Java concurrency and Project Loom virtual-thread adoption for correctness and safety at scale — pooling or capping virtual threads, a virtual-thread migration that strips a bound on a limited downstream resource (connection pool, rate-limited API) without re-imposing it via a Semaphore, JDK-version-gated carrier-pinning risk from synchronized blocks (materially reduced from JDK 24 onward by JEP 491 versus JDK 21's JEP 444), ThreadLocal cost at millions-of-threads scale, StructuredTaskScope's preview status, and classic concurrency hygiene (visibility/atomicity, unbounded ExecutorService queues, ThreadLocal leaks in pooled executors). Trigger when a user provides Java source using Thread.ofVirtual/Executors.newVirtualThreadPerTaskExecutor, ExecutorService construction, synchronized blocks, or JFR pinning output and asks whether their virtual-thread usage or concurrency code is correct. Reads source and sanitized configuration only; it never runs a build, invokes a JDK, attaches a profiler, or contacts a live system. |
| allowed-tools | Read Grep Glob |
| metadata | {"author":"github: Raishin","version":"0.1.0","updated":"2026-07-17","category":"compute","lifecycle":"experimental"} |
java-concurrency-and-virtual-thread
Purpose
This skill statically assesses whether a Java codebase's virtual-thread adoption is correct and safe at scale, and whether its classic concurrency code is free of visibility, atomicity, and resource-exhaustion bugs. Virtual threads change the cost model of concurrency (creation is cheap, blocking is cheap) but do not remove the need to bound access to genuinely limited resources, and they introduce a JDK-version-dependent carrier-pinning hazard that is easy to over- or under-state without evidence. The review distinguishes what virtual threads fix (thread-creation cost, thread-count ceilings) from what they do not fix (the finite capacity of a downstream connection pool or rate-limited API), and gates every pinning claim on the JDK version and JFR evidence rather than blanket JDK-21-era folklore.
Trigger conditions
- A user provides Java source using Thread.ofVirtual, Thread.ofVirtual().start()/factory(), or Executors.newVirtualThreadPerTaskExecutor() and asks whether the usage is correct or safe at scale.
- A user is migrating (or has migrated) a platform-thread-pool-based service to virtual threads and asks about connection-pool, rate-limiter, or other downstream-resource behavior after the migration.
- A user provides JFR jdk.VirtualThreadPinned output, jdk.tracePinnedThreads output, or reports thread-pinning/scalability symptoms and asks for a diagnosis.
- A user asks about StructuredTaskScope, ScopedValue, or classic ExecutorService/ThreadLocal correctness in Java source.
When not to use
- The task is JDK vendor/version lifecycle or upgrade sequencing itself (support boundaries, license exposure, removed-API migration) — route to java-jdk-lifecycle-and-upgrade-agent; this skill only consumes the JDK version as a gating input.
- The task is JPA/Hibernate connection-pool sizing or HikariCP tuning mechanics — route to java-jpa-hibernate-performance-agent; this skill only flags that a concurrency bound must exist, not what its numeric size should be.
- The task is deserializing or parsing untrusted input — route to java-deserialization-and-parser-security-agent.
- The task asks to actually run a build, attach a profiler/JFR recorder, or execute code against a live system — this skill is static-review only.
Lean operating rules
- CRITICAL — Flag pooling or reusing virtual-thread instances (any fixed-size executor wrapping Thread.ofVirtual, or manual reuse of a virtual Thread object across tasks) as an anti-pattern — create a new virtual thread per task via Thread.ofVirtual().start() or Executors.newVirtualThreadPerTaskExecutor(); pooling defeats the cheap-creation model and adds no benefit over platform threads.
- CRITICAL — Flag capping virtual-thread concurrency behind a small fixed-size pool, or a bounded queue placed in front of the virtual-thread executor, as defeating the migration's purpose — if a cap is genuinely needed it belongs on the downstream resource, not on thread creation.
- CRITICAL — When a virtual-thread migration removes an implicit concurrency bound a platform-thread pool used to provide (connection pool, rate-limited API), require the bound be explicitly re-imposed with a Semaphore or equivalent sized to the resource's real capacity — do not accept the migration itself as reintroducing the bound; unbounded virtual-thread concurrency exhausts the resource.
- HIGH — Gate every carrier-pinning claim on the JDK version in scope: JDK 21 through 23 (JEP 444) — synchronized blocks/methods and native/foreign-function calls pin the carrier for the duration of any blocking operation performed inside them. JDK 24 and later (JEP 491) — synchronized no longer pins for ordinary monitor acquisition or Object.wait, but native-method and Foreign Function & Memory calls still pin on every version. Never give a pinning verdict without naming the JDK version.
- HIGH — Do not assert pinning is occurring, or that it has been fixed, without JFR jdk.VirtualThreadPinned evidence or jdk.tracePinnedThreads output the user supplies; a synchronized block found in source is pinning risk, not confirmed pinning.
- HIGH — Do not carry JDK-21-era pinning advice forward unchanged onto JDK-24+ code; re-derive the verdict from the JDK version actually in scope every time.
- HIGH — Flag ThreadLocal or InheritableThreadLocal usage sized for a small platform-thread pool that is carried unchanged into a virtual-thread-per-task model; at millions of virtual threads this becomes real memory and GC pressure — recommend ScopedValue (confirm it is GA, not preview, for the JDK version in scope) or task-scoped state instead of thread-scoped state.
- MEDIUM — Treat StructuredTaskScope and related structured-concurrency APIs as preview features requiring --enable-preview and an explicit JDK-version/preview-iteration confirmation; never present them as stable or assume the API shape is final, since the preview form has changed across releases.
- HIGH — Flag unbounded ExecutorService work queues (e.g. a fixed or cached thread pool backed by an unbounded LinkedBlockingQueue) as an OOM or latency-cliff risk under load; require a bounded queue and a defined rejection policy.
- HIGH — Flag ThreadLocal values set on a pooled-platform-thread executor that are not cleared in a finally block; they leak across tasks that reuse the same platform thread, including leaking stale or security-sensitive state.
References
Load these only when needed:
Response minimum
Return, at minimum:
- A verdict (pass / pass-with-conditions / block).
- The JDK version(s) in scope and which pinning regime applies (JEP 444 pre-24 vs JEP 491 24+, or unknown).
- Virtual-thread lifecycle findings (pooling/capping) and downstream-resource bound findings, each severity- and evidence-basis-labelled.
- Carrier-pinning findings, explicitly noting whether JFR jdk.VirtualThreadPinned evidence was supplied or the finding is source-level risk only.
- Classic-concurrency findings (visibility/atomicity, unbounded queues, ThreadLocal leaks in pooled executors).
- Safe next actions and open questions, including any JDK version or JFR evidence the user must supply.