到達点は、並行処理の必要性と競合の原因を説明することです。前提は第08・13回です。
同時進行と同時実行
並行とは複数の作業を進める構造、並列とは複数の計算を実際に同時実行することです。ネットワーク待ちの多い処理では、待っている間に別の仕事を進める価値があります。Java 21の仮想スレッドは、このような多数の待ちを伴う仕事で候補になります。CPUそのものを増やす機能ではありません。
import java.util.ArrayList;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) throws Exception {
AtomicInteger count = new AtomicInteger();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = new ArrayList<Future<?>>();
for (int i = 0; i < 20; i++) {
futures.add(executor.submit(() -> {
for (int j = 0; j < 1000; j++) count.incrementAndGet();
}));
}
for (Future<?> future : futures) future.get();
}
System.out.println(count.get());
}
}
Main.javaの出力は20000です。Future.getで仕事の完了と失敗を確認します。これは共有カウンタの実習であり、仮想スレッドの速度を測るベンチマークではありません。
足し算でも競合する
通常の count++ は、読む、増やす、書くという複数の段階からなります。二つのスレッドが同じ古い値を読むと、一方の増分が失われます。AtomicIntegerは今回の一回の加算を原子的に行います。volatileだけでは、この複合操作を原子的にはできません。
複数の値をまとめて守るなら、同じロックで囲むsynchronizedなどが必要です。「在庫があるか確認」と「在庫を減らす」を別々に同期しても、その間へ他の処理が入れます。ロックの対象は変数ではなく、守りたい一連の操作から決めます。
外部サービスへ大量の仕事を投げる場合は、スレッドが軽量でも接続数や同時実行数を制限します。中断要求を受けたときは、適切に伝播するか割り込み状態を戻します。停止を無視すると、タイムアウト後も処理が残ります。
練習と解答
練習:AtomicIntegerを普通のint配列の先頭要素に変え、毎回20000にならない可能性を説明します。
解答:共有値への更新が競合します。何度か20000になっても正しさの証明にはなりません。修正案はAtomicInteger、同じロックによる保護、各タスクで局所集計して最後に合算する方法です。
公式資料
Java全20回の目次 | 前の回 | 次の回