| name | concurrency-review |
| description | 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. |
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
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Request request : requests) {
executor.submit(() -> callExternalApi(request));
}
}
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).
synchronized (lock) {
blockingIoCall();
}
ScopedValue Over ThreadLocal
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
processRequest();
});
Structured Concurrency (Java 25 Preview)
try (StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(id));
Subtask<Orders> ordersTask = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new Profile(userTask.get(), ordersTask.get());
}
Spring @Async Pitfalls
1. Forgetting @EnableAsync
@Service
public class EmailService {
@Async
public void sendEmail(String to) { }
}
@Configuration
@EnableAsync
public class AsyncConfig { }
2. Calling Async from Same Class
@Service
public class OrderService {
public void processOrder(Order order) {
sendConfirmation(order);
}
@Async
public void sendConfirmation(Order order) { }
}
@Service
public class OrderService {
@Autowired
private EmailService emailService;
public void processOrder(Order order) {
emailService.sendConfirmation(order);
}
}
3. @Async on Non-Public Methods
@Async
private void processInBackground() { }
@Async
protected void processInBackground() { }
@Async
public void processInBackground() { }
4. Default Executor Creates Thread Per Task
@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
@Async
public void auditAction() {
String user = SecurityContextHolder.getContext().getAuthentication().getName();
}
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
CompletableFuture Patterns
Error Handling
CompletableFuture.supplyAsync(() -> riskyOperation());
CompletableFuture.supplyAsync(() -> riskyOperation())
.exceptionally(ex -> {
log.error("Operation failed", ex);
return fallbackValue;
});
CompletableFuture.supplyAsync(() -> riskyOperation())
.handle((result, ex) -> {
if (ex != null) {
log.error("Failed", ex);
return fallbackValue;
}
return result;
});
Timeout Handling (Java 9+)
CompletableFuture.supplyAsync(() -> slowOperation())
.orTimeout(5, TimeUnit.SECONDS);
CompletableFuture.supplyAsync(() -> slowOperation())
.completeOnTimeout(defaultValue, 5, TimeUnit.SECONDS);
Combining Futures
CompletableFuture.allOf(future1, future2, future3)
.thenRun(() -> log.info("All completed"));
CompletableFuture.anyOf(future1, future2, future3)
.thenAccept(result -> log.info("First result: {}", result));
future1.thenCombine(future2, (r1, r2) -> merge(r1, r2));
Use Appropriate Executor
CompletableFuture.supplyAsync(() -> cpuIntensiveWork());
ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> blockingIoCall(), ioExecutor);
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture.supplyAsync(() -> blockingIoCall(), virtualExecutor);
Classic Concurrency Issues
Race Conditions: Check-Then-Act
if (!map.containsKey(key)) {
map.put(key, computeValue());
}
map.computeIfAbsent(key, k -> computeValue());
if (count < MAX) {
count++;
}
AtomicInteger count = new AtomicInteger();
count.updateAndGet(c -> c < MAX ? c + 1 : c);
Visibility: Missing volatile
private boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) { }
}
private volatile boolean running = true;
Non-Atomic long/double
private long counter;
public void increment() {
counter++;
}
private AtomicLong counter = new AtomicLong();
private volatile long counter;
Double-Checked Locking
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private static volatile Singleton instance;
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
Deadlocks: Lock Ordering
public void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
}
}
}
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) {
}
}
}
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
if (!map.containsKey(key)) {
map.put(key, value);
}
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> createValue());
map.compute(key1, (k, v) -> {
return map.compute(key2, ...);
});
Concurrency Review Checklist
🔴 High Severity (Likely Bugs)
🟡 Medium Severity (Potential Issues)
🟢 Modern Patterns (Java 21/25)
📝 Documentation
Analysis Commands
grep -rn "synchronized" --include="*.java"
grep -rn "@Async" --include="*.java"
grep -rn "volatile" --include="*.java"
grep -rn "Executors\.\|ThreadPoolExecutor\|ExecutorService" --include="*.java"
grep -rn "CompletableFuture\." --include="*.java" | grep -v "exceptionally\|handle\|whenComplete"
grep -rn "ThreadLocal" --include="*.java"