$npx -y skills add decebals/claude-code-java --skill concurrency-reviewReview 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.
| 1 | # Concurrency Review Skill |
| 2 | |
| 3 | Review Java concurrent code for correctness, safety, and modern best practices. |
| 4 | |
| 5 | ## Why This Matters |
| 6 | |
| 7 | > Nearly 60% of multithreaded applications encounter issues due to improper management of shared resources. - ACM Study |
| 8 | |
| 9 | Concurrency bugs are: |
| 10 | - **Hard to reproduce** - timing-dependent |
| 11 | - **Hard to test** - may only appear under load |
| 12 | - **Hard to debug** - non-deterministic behavior |
| 13 | |
| 14 | This skill helps catch issues **before** they reach production. |
| 15 | |
| 16 | ## When to Use |
| 17 | - Reviewing code with `synchronized`, `volatile`, `Lock` |
| 18 | - Checking `@Async`, `CompletableFuture`, `ExecutorService` |
| 19 | - Validating thread safety of shared state |
| 20 | - Reviewing Virtual Threads / Structured Concurrency code |
| 21 | - Any code accessed by multiple threads |
| 22 | |
| 23 | --- |
| 24 | |
| 25 | ## Modern Java (21/25): Virtual Threads |
| 26 | |
| 27 | ### When to Use Virtual Threads |
| 28 | |
| 29 | ```java |
| 30 | // ✅ Perfect for I/O-bound tasks (HTTP, DB, file I/O) |
| 31 | try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { |
| 32 | for (Request request : requests) { |
| 33 | executor.submit(() -> callExternalApi(request)); |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // ❌ Not beneficial for CPU-bound tasks |
| 38 | // Use platform threads / ForkJoinPool instead |
| 39 | ``` |
| 40 | |
| 41 | **Rule of thumb**: If your app never has 10,000+ concurrent tasks, virtual threads may not provide significant benefit. |
| 42 | |
| 43 | ### Java 25: Synchronized Pinning Fixed |
| 44 | |
| 45 | In Java 21-23, virtual threads became "pinned" when entering `synchronized` blocks with blocking operations. **Java 25 fixes this** (JEP 491). |
| 46 | |
| 47 | ```java |
| 48 | // In Java 21-23: ⚠️ Could cause pinning |
| 49 | synchronized (lock) { |
| 50 | blockingIoCall(); // Virtual thread pinned to carrier |
| 51 | } |
| 52 | |
| 53 | // In Java 25: ✅ No longer an issue |
| 54 | // But consider ReentrantLock for explicit control anyway |
| 55 | ``` |
| 56 | |
| 57 | ### ScopedValue Over ThreadLocal |
| 58 | |
| 59 | ```java |
| 60 | // ❌ ThreadLocal problematic with virtual threads |
| 61 | private static final ThreadLocal<User> currentUser = new ThreadLocal<>(); |
| 62 | |
| 63 | // ✅ ScopedValue (Java 21+ preview, improved in 25) |
| 64 | private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance(); |
| 65 | |
| 66 | ScopedValue.where(CURRENT_USER, user).run(() -> { |
| 67 | // CURRENT_USER.get() available here and in child virtual threads |
| 68 | processRequest(); |
| 69 | }); |
| 70 | ``` |
| 71 | |
| 72 | ### Structured Concurrency (Java 25 Preview) |
| 73 | |
| 74 | ```java |
| 75 | // ✅ Structured concurrency - tasks tied to scope lifecycle |
| 76 | try (StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure()) { |
| 77 | Subtask<User> userTask = scope.fork(() -> fetchUser(id)); |
| 78 | Subtask<Orders> ordersTask = scope.fork(() -> fetchOrders(id)); |
| 79 | |
| 80 | scope.join(); // Wait for all |
| 81 | scope.throwIfFailed(); // Propagate exceptions |
| 82 | |
| 83 | return new Profile(userTask.get(), ordersTask.get()); |
| 84 | } |
| 85 | // All subtasks automatically cancelled if scope exits |
| 86 | ``` |
| 87 | |
| 88 | --- |
| 89 | |
| 90 | ## Spring @Async Pitfalls |
| 91 | |
| 92 | ### 1. Forgetting @EnableAsync |
| 93 | |
| 94 | ```java |
| 95 | // ❌ @Async silently ignored |
| 96 | @Service |
| 97 | public class EmailService { |
| 98 | @Async |
| 99 | public void sendEmail(String to) { } |
| 100 | } |
| 101 | |
| 102 | // ✅ Enable async processing |
| 103 | @Configuration |
| 104 | @EnableAsync |
| 105 | public class AsyncConfig { } |
| 106 | ``` |
| 107 | |
| 108 | ### 2. Calling Async from Same Class |
| 109 | |
| 110 | ```java |
| 111 | @Service |
| 112 | public class OrderService { |
| 113 | |
| 114 | // ❌ Bypasses proxy - runs synchronously! |
| 115 | public void processOrder(Order order) { |
| 116 | sendConfirmation(order); // Direct call, not async |
| 117 | } |
| 118 | |
| 119 | @Async |
| 120 | public void sendConfirmation(Order order) { } |
| 121 | } |
| 122 | |
| 123 | // ✅ Inject self or use separate service |
| 124 | @Service |
| 125 | public class OrderService { |
| 126 | @Autowired |
| 127 | private EmailService emailService; // Separate bean |
| 128 | |
| 129 | public void processOrder(Order order) { |
| 130 | emailService.sendConfirmation(order); // Proxy call, async works |
| 131 | } |
| 132 | } |
| 133 | ``` |
| 134 | |
| 135 | ### 3. @Async on Non-Public Methods |
| 136 | |
| 137 | ```java |
| 138 | // ❌ Non-public methods - proxy can't intercept |
| 139 | @Async |
| 140 | private void processInBackground() { } |
| 141 | |
| 142 | @Async |
| 143 | protected void processInBackground() { } |
| 144 | |
| 145 | // ✅ Must be public |
| 146 | @Async |
| 147 | public void processInBackground() { } |
| 148 | ``` |
| 149 | |
| 150 | ### 4. Default Executor Creates Thread Per Task |
| 151 | |
| 152 | ```java |
| 153 | // ❌ Default SimpleAsyncTaskExecutor - creates new thread each time! |
| 154 | // Can cause OutOfMemoryError under load |
| 155 | |
| 156 | // ✅ Configure proper thread pool |
| 157 | @Configuration |
| 158 | @EnableAsync |
| 159 | public class AsyncConfig { |
| 160 | |
| 161 | @Bean |
| 162 | public Executor taskExecutor() { |
| 163 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); |
| 164 | executor.setCorePoolSize(10); |
| 165 | executor.setMaxPoolSize(50); |
| 166 | executor.setQueueCapacity(100); |
| 167 | executor.setThreadNamePrefix("async-"); |
| 168 | executor.setRejectedExecutionHandler(new CallerRunsPolicy()); |
| 169 | executor.initialize(); |
| 170 | return executor; |
| 171 | } |
| 172 | } |
| 173 | ``` |
| 174 | |
| 175 | ### 5. SecurityContext Not Propagating |
| 176 | |
| 177 | ```java |
| 178 | // ❌ SecurityContextHolder is ThreadLocal-bound |
| 179 | @Async |
| 180 | public void auditAction() { |
| 181 | // SecurityContextHol |