concurrency-review

Concurrency Review Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "concurrency-review" with this command: npx skills add decebals/claude-code-java/decebals-claude-code-java-concurrency-review

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 { }

  1. 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
}

}

  1. @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() { }

  1. 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;
}

}

  1. 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 synchronized calling external/unknown code (deadlock risk)

  • volatile present 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

  • ExecutorService properly 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 volatile usage 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"

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

java-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

spring-boot-patterns

No summary provided by upstream source.

Repository SourceNeeds Review