
concurrency-review
Review Java concurrency code for thread safety, race conditions, deadlocks, and modern patterns (Virtual Threads, CompletableFuture, @Async). Use when user asks "check thread safety", "concurrency review", "async code review", or when reviewing multi-threaded code.
- Installs
- 355
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/decebals/claude-code-java --skill concurrency-review- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads concurrency-review automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Concurrency Review Skill
Review Java concurrent code for correctness, safety, and modern best practices.
Why This Matters
Nearly 60% of multithreaded applications encounter issues due to improper management of shared resources. - ACM Study
Concurrency bugs are:
- Hard to reproduce - timing-dependent
- Hard to test - may only appear under load
- Hard to debug - non-deterministic behavior
This skill helps catch issues before they reach production.
When to Use
- Reviewing code with
synchronized,volatile,Lock - Checking
@Async,CompletableFuture,ExecutorService - Validating thread safety of shared state
- Reviewing Virtual Threads / Structured Concurrency code
- Any code accessed by multiple threads
Modern Java (21/25): Virtual Threads
When to Use Virtual Threads
// ✅ Perfect for I/O-bound tasks (HTTP, DB, file I/O)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Request request : requests) {
executor.submit(() -> callExternalApi(request));
}
}
// ❌ Not beneficial for CPU-bound tasks
// Use platform threads / ForkJoinPool instead
Rule of thumb: If your app never has 10,000+ concurrent tasks, virtual threads may not provide significant benefit.
Java 25: Synchronized Pinning Fixed
In Java 21-23, virtual threads became "pinned" when entering synchronized blocks with blocking operations. Java 25 fixes this (JEP 491).
// In Java 21-23: ⚠️ Could cause pinning
synchronized (lock) {
blockingIoCall(); // Virtual thread pinned to carrier
}
// In Java 25: ✅ No longer an issue
// But consider ReentrantLock for explicit control anyway
ScopedValue Over ThreadLocal
// ❌ ThreadLocal problematic with virtual threads
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
// ✅ ScopedValue (Java 21+ preview, improved in 25)
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
// CURRENT_USER.get() available here and in child virtual threads
processRequest();
});
Structured Concurrency (Java 25 Preview)
// ✅ Structured concurrency - tasks tied to scope lifecycle
try (StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(id));
Subtask<Orders> ordersTask = scope.fork(() -> fetchOrders(id));
scope.join(); // Wait for all
scope.throwIfFailed(); // Propagate exceptions
return new Profile(userTask.get(), ordersTask.get());
}
// All subtasks automatically cancelled if scope exits
Spring @Async Pitfalls
1. Forgetting @EnableAsync
// ❌ @Async silently ignored
@Service
public class EmailService {
@Async
public void sendEmail(String to) { }
}
// ✅ Enable async processing
@Configuration
@EnableAsync
public class AsyncConfig { }
2. Calling Async from Same Class
@Service
public class OrderService {
// ❌ Bypasses proxy - runs synchronously!
public void processOrder(Order order) {
sendConfirmation(order); // Direct call, not async
}
@Async
public void sendConfirmation(Order order) { }
}
// ✅ Inject self or use separate service
@Service
public class OrderService {
@Autowired
private EmailService emailService; // Separate bean
public void processOrder(Order order) {
emailService.sendConfirmation(order); // Proxy call, async works
}
}
3. @Async on Non-Public Methods
// ❌ Non-public methods - proxy can't intercept
@Async
private void processInBackground() { }
@Async
protected void processInBackground() { }
// ✅ Must be public
@Async
public void processInBackground() { }
4. Default Executor Creates Thread Per Task
// ❌ Default SimpleAsyncTaskExecutor - creates new thread each time!
// Can cause OutOfMemoryError under load
// ✅ Configure proper thread pool
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
executor.initialize();
return executor;
}
}
5. SecurityContext Not Propagating
// ❌ SecurityContextHolder is ThreadLocal-bound
@Async
public void auditAction() {
// SecurityContextHolder.getContext() is NULL here!
String user = SecurityContextHolder.getContext().getAuthentication().getName();
}
// ✅ Use DelegatingSecurityContextAsyncTaskExecutor
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ... configure ...
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
CompletableFuture Patterns
Error Handling
// ❌ Exception silently swallowed
CompletableFuture.supplyAsync(() -> riskyOperation());
// If riskyOperation throws, nobody knows
// ✅ Always handle exceptions
CompletableFuture.supplyAsync(() -> riskyOperation())
.exceptionally(ex -> {
log.error("Operation failed", ex);
return fallbackValue;
});
// ✅ Or use handle() for both success and failure
CompletableFuture.supplyAsync(() -> riskyOperation())
.handle((result, ex) -> {
if (ex != null) {
log.error("Failed", ex);
return fallbackValue;
}
return result;
});
Timeout Handling (Java 9+)
// ✅ Fail after timeout
CompletableFuture.supplyAsync(() -> slowOperation())
.orTimeout(5, TimeUnit.SECONDS); // Throws TimeoutException
// ✅ Return default after timeout
CompletableFuture.supplyAsync(() -> slowOperation())
.completeOnTimeout(defaultValue, 5, TimeUnit.SECONDS);
Combining Futures
// ✅ Wait for all
CompletableFuture.allOf(future1, future2, future3)
.thenRun(() -> log.info("All completed"));
// ✅ Wait for first
CompletableFuture.anyOf(future1, future2, future3)
.thenAccept(result -> log.info("First result: {}", result));
// ✅ Combine results
future1.thenCombine(future2, (r1, r2) -> merge(r1, r2));
Use Appropriate Executor
// ❌ CPU-bound task in ForkJoinPool.commonPool (default)
CompletableFuture.supplyAsync(() -> cpuIntensiveWork());
// ✅ Custom executor for blocking/I/O operations
ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> blockingIoCall(), ioExecutor);
// ✅ In Java 21+, virtual threads for I/O
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture.supplyAsync(() -> blockingIoCall(), virtualExecutor);
Classic Concurrency Issues
Race Conditions: Check-Then-Act
// ❌ Race condition
if (!map.containsKey(key)) {
map.put(key, computeValue()); // Another thread may have added it
}
// ✅ Atomic operation
map.computeIfAbsent(key, k -> computeValue());
// ❌ Race condition with counter
if (count < MAX) {
count++; // Read-check-write is not atomic
}
// ✅ Atomic counter
AtomicInteger count = new AtomicInteger();
count.updateAndGet(c -> c < MAX ? c + 1 : c);
Visibility: Missing volatile
// ❌ Other threads may never see the update
private boolean running = true;
public void stop() {
running = false; // May not be visible to other threads
}
public void run() {
while (running) { } // May loop forever
}
// ✅ Volatile ensures visibility
private volatile boolean running = true;
Non-Atomic long/double
// ❌ 64-bit read/write is non-atomic on 32-bit JVMs
private long counter;
public void increment() {
counter++; // Not atomic!
}
// ✅ Use AtomicLong or synchronization
private AtomicLong counter = new AtomicLong();
// ✅ Or volatile (for single-writer scenarios)
private volatile long counter;
Double-Checked Locking
// ❌ Broken without volatile
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // May be seen partially constructed
}
}
}
return instance;
}
// ✅ Correct with volatile
private static volatile Singleton instance;
// ✅ Or use holder class idiom
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
Deadlocks: Lock Ordering
// ❌ Potential deadlock
// Thread 1: lock(A) -> lock(B)
// Thread 2: lock(B) -> lock(A)
public void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
// Transfer logic
}
}
}
// ✅ Consistent lock ordering
public void transfer(Account from, Account to, int amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
// Transfer logic
}
}
}
Thread-Safe Collections
Choose the Right Collection
| Use Case | Wrong | Right |
|---|---|---|
| Concurrent reads/writes | HashMap | ConcurrentHashMap |
| Frequent iteration | ConcurrentHashMap | CopyOnWriteArrayList |
| Producer-consumer | ArrayList | BlockingQueue |
| Sorted concurrent | TreeMap | ConcurrentSkipListMap |
ConcurrentHashMap Pitfalls
// ❌ Non-atomic compound operation
if (!map.containsKey(key)) {
map.put(key, value);
}
// ✅ Atomic
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> createValue());
// ❌ Nested compute can deadlock
map.compute(key1, (k, v) -> {
return map.compute(key2, ...); // Deadlock risk!
});
Concurrency Review Checklist
🔴 High Severity (Likely Bugs)
- No check-then-act on shared state without synchronization
- No
synchronizedcalling external/unknown code (deadlock risk) -
volatilepresent for double-checked locking - Non-volatile fields not read in loops waiting for updates
-
ConcurrentHashMap.compute()doesn't call other map operations - @Async methods are public and called from different beans
🟡 Medium Severity (Potential Issues)
- Thread pools properly sized and named
- CompletableFuture exceptions handled (exceptionally/handle)
- SecurityContext propagated to async tasks if needed
-
ExecutorServiceproperly shut down -
Lock.unlock()in finally block - Thread-safe collections used for shared data
🟢 Modern Patterns (Java 21/25)
- Virtual threads used for I/O-bound concurrent tasks
- ScopedValue considered over ThreadLocal
- Structured concurrency for related subtasks
- Timeouts on CompletableFuture operations
📝 Documentation
- Thread safety documented on shared classes
- Locking order documented for nested locks
- Each
volatileusage justified
Analysis Commands
# Find synchronized blocks
grep -rn "synchronized" --include="*.java"
# Find @Async methods
grep -rn "@Async" --include="*.java"
# Find volatile fields
grep -rn "volatile" --include="*.java"
# Find thread pool creation
grep -rn "Executors\.\|ThreadPoolExecutor\|ExecutorService" --include="*.java"
# Find CompletableFuture without error handling
grep -rn "CompletableFuture\." --include="*.java" | grep -v "exceptionally\|handle\|whenComplete"
# Find ThreadLocal (consider ScopedValue in Java 21+)
grep -rn "ThreadLocal" --include="*.java"
FAQ
What does the concurrency-review skill do?
Review Java concurrency code for thread safety, race conditions, deadlocks, and modern patterns (Virtual Threads, CompletableFuture, @Async). Use when user asks "check thread safety", "concurrency review", "async code review", or when reviewing multi-threaded code.
How do I install concurrency-review?
Run npx skills add https://github.com/decebals/claude-code-java --skill concurrency-review in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/concurrency-review/ (personal) or .claude/skills/concurrency-review/ (project).
Which AI agents can use concurrency-review?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is concurrency-review safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains concurrency-review?
It is published by decebals in decebals/claude-code-java. It has 355 recorded installs.
More from decebals/claude-code-java
java-code-review
decebals/claude-code-java
Systematic code review for Java with null safety, exception handling, concurrency, and performance checks. Use when user says "review code", "check this PR", "code review", or before merging changes.
clean-code
decebals/claude-code-java
Clean Code principles (DRY, KISS, YAGNI), naming, function design and readability. Use when code is hard to read, with long methods, unclear names, duplication or deep nesting. For how responsibilities are split across classes and which way dependencies point, use solid-principles instead.
solid-principles
decebals/claude-code-java
SOLID principles checklist with Java examples. Use when a class has too many responsibilities, an abstraction leaks, or a dependency points the wrong way, and when the user asks about Single Responsibility, Open/Closed, Liskov, Interface Segregation or Dependency Inversion. For naming, duplication and method length, use clean-code instead.
architecture-review
decebals/claude-code-java
Analyze Java project architecture at macro level - package structure, module boundaries, dependency direction, and layering. Use when user asks "review architecture", "check structure", "package organization", or when evaluating if a codebase follows clean architecture principles.
design-patterns
decebals/claude-code-java
Open the skill to read its SKILL.md instructions.
spring-boot-patterns
decebals/claude-code-java
Spring Boot best practices and patterns. Use when creating controllers, services or repositories, or when the user asks about Spring Boot layering, wiring, configuration or exception handling. For JPA and Hibernate behaviour, use jpa-patterns instead.
Popular code quality & workflow skills
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
improve-codebase-architecture
mattpocock/skills
Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
handoff
mattpocock/skills
Compact the current conversation into a handoff document for another agent to pick up.
domain-modeling
mattpocock/skills
Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a GLOSSARY.md, or recording or editing an ADR.
codebase-design
mattpocock/skills
Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
diagnosing-bugs
mattpocock/skills
Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.