concurrency-review について
マルチスレッドコードを確認する Java 開発者向けの並行処理レビュー用スキルです。一般的な不具合のチェックリストに加え、最新の Java 並行処理や Spring の非同期処理に関する注意点を扱います。
主な機能
- 01競合状態と可視性の確認
- 02Spring @Async の注意点
- 03CompletableFuture のエラーとタイムアウト処理
- 04仮想スレッドと構造化並行処理の指針
- 05スレッドセーフなコレクションの選択
活用シーン
- 01共有状態のスレッド安全性レビュー
- 02Spring の非同期メソッドの確認
- 03CompletableFuture のエラー処理の評価
- 04Java 21/25 の並行処理コードのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ decebals/ claude-code-java --skill concurrency-reviewインストール後、Claude Code で/concurrency-reviewを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
並行処理レビュースキル
Java の並行処理コードを、正確性、安全性、最新のベストプラクティスの観点からレビューします。
重要な理由
マルチスレッドアプリケーションの約 60% で、共有リソースの管理が不適切なために問題が発生します。- ACM の調査
並行処理のバグには、次のような特徴があります。
- 再現が難しい - タイミングに依存します
- テストが難しい - 負荷がかかったときにしか現れないことがあります
- デバッグが難しい - 動作が非決定的です
このスキルは、本番環境に到達する前に問題を見つけるのに役立ちます。
使用するタイミング
synchronized、volatile、Lockを使ったコードをレビューするとき@Async、CompletableFuture、ExecutorServiceを確認するとき- 共有状態のスレッドセーフ性を検証するとき
- 仮想スレッド/構造化並行処理のコードをレビューするとき
- 複数のスレッドからアクセスされるコード全般
最新の Java(21/25):仮想スレッド
仮想スレッドを使うタイミング
// ✅ 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
経験則:アプリで 10,000 個以上のタスクを同時に実行することがない場合、仮想スレッドによる大きな効果は得られない可能性があります。
Java 25:synchronized によるピン留めを修正
Java 21~23 では、ブロッキング処理を含む synchronized ブロックに入ると、仮想スレッドが「ピン留め」されるようになっていました。Java 25 で修正されています(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
ThreadLocal より ScopedValue を優先
// ❌ 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();
});
構造化並行処理(Java 25 プレビュー)
// ✅ 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 に関する落とし穴
1. @EnableAsync の指定忘れ
// ❌ @Async silently ignored
@Service
public class EmailService {
@Async
public void sendEmail(String to) { }
}
// ✅ Enable async processing
@Configuration
@EnableAsync
public class AsyncConfig { }
2. 同じクラスから非同期メソッドを呼び出す
@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. public 以外のメソッドに @Async を指定する
// ❌ Non-public methods - proxy can't intercept
@Async
private void processInBackground() { }
@Async
protected void processInBackground() { }
// ✅ Must be public
@Async
public void processInBackground() { }
4. デフォルトの Executor はタスクごとにスレッドを作成する
// ❌ 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 が伝播しない
// ❌ 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 のパターン
エラー処理
// ❌ 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;
});
タイムアウト処理(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);
Future の結合
// ✅ 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));
適切な 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);
従来からある並行処理の問題
競合状態:確認してから実行する
// ❌ 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);
可視性: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;
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;
ダブルチェックロック
// ❌ 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;
}
デッドロック:ロックの順序
// ❌ 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
}
}
}
スレッドセーフなコレクション
適切なコレクションを選ぶ
| 用途 | 不適切 | 適切 |
|---|---|---|
| 並行読み取り/書き込み | HashMap | ConcurrentHashMap |
| 反復処理が頻繁 | ConcurrentHashMap | CopyOnWriteArrayList |
| プロデューサーとコンシューマー | ArrayList | BlockingQueue |
| 並行処理とソート | TreeMap | ConcurrentSkipListMap |
ConcurrentHashMap に関する落とし穴
// ❌ 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!
});
並行処理レビューのチェックリスト
🔴 重大度:高(バグの可能性が高い)
- 共有状態に対して、同期せずに確認してから実行する処理がない
-
synchronizedの中で外部コードや未知のコードを呼び出していない(デッドロックの危険性) - ダブルチェックロックで
volatileを指定している - 更新を待つループで、非 volatile フィールドを読み取っていない
-
ConcurrentHashMap.compute()から他のマップ操作を呼び出していない - @Async メソッドが public で、異なる Bean から呼び出されている
🟡 重大度:中(潜在的な問題)
- スレッドプールのサイズと名前が適切である
- CompletableFuture の例外が処理されている(exceptionally/handle)
- 必要に応じて SecurityContext が非同期タスクに伝播されている
-
ExecutorServiceが適切にシャットダウンされている -
Lock.unlock()が finally ブロック内にある - 共有データにスレッドセーフなコレクションを使用している
🟢 最新のパターン(Java 21/25)
- I/O バウンドの並行タスクに仮想スレッドを使用している
- ThreadLocal より ScopedValue を使うことを検討している
- 関連するサブタスクに構造化並行処理を使用している
- CompletableFuture の操作にタイムアウトを設定している
📝 ドキュメント
- 共有クラスのスレッドセーフ性を文書化している
- ネストしたロックのロック順序を文書化している
- 各
volatileの使用理由を説明している
分析コマンド
# 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"
コメント
Markdown 対応