Java
Z
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
Javaマルチスレッドプログラミング入門 — Thread/ExecutorService/CompletableFuture
Javaのスレッド基礎、ExecutorServiceによるスレッドプール、CompletableFutureによる非同期処理、スレッドセーフの実現方法を解説します。
一言結論
Javaの並行処理ではnew Thread()を直接使うのは避けてExecutorServiceでスレッドプールを管理し、複数の非同期処理を合成する場合はCompletableFutureを使うのが現代の正しいアプローチだ。
スレッドの基本
// Thread クラスを使う方法
Thread thread = new Thread(() -> {
System.out.println("別スレッドで実行");
});
thread.start();
// Runnable を使う方法(推奨)
Runnable task = () -> System.out.println("タスク実行");
Thread thread = new Thread(task);
thread.start();
// スレッドの完了を待つ
thread.join();
ExecutorService(スレッドプール)
スレッドを都度 new Thread() するのは非効率です。ExecutorService でスレッドプールを使いまわします。
// 固定サイズのスレッドプール
ExecutorService executor = Executors.newFixedThreadPool(4);
// タスクを投入
executor.execute(() -> processTask("A"));
executor.execute(() -> processTask("B"));
// 終了処理
executor.shutdown(); // 新規タスクを受け付けなくなる
executor.awaitTermination(30, TimeUnit.SECONDS); // 終了を待つ
Executors のファクトリメソッド
Executors.newFixedThreadPool(n) // 固定サイズ
Executors.newCachedThreadPool() // 必要に応じて増減(短命タスク向け)
Executors.newSingleThreadExecutor() // シングルスレッド(順番保証)
Executors.newScheduledThreadPool(n) // 定期実行
Callable と Future
Callable は結果を返すタスクです。
ExecutorService executor = Executors.newFixedThreadPool(4);
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
Future<Integer> future = executor.submit(task);
// 他の処理...
// 結果を取得(完了するまでブロック)
Integer result = future.get();
System.out.println(result); // 42
// タイムアウト付き
Integer result = future.get(2, TimeUnit.SECONDS);
CompletableFuture(非同期処理)
Java 8 で導入された、より柔軟な非同期処理の仕組みです。
// 非同期タスクを実行
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 重い処理
return fetchDataFromAPI();
});
// 完了後に変換
CompletableFuture<Integer> length = future.thenApply(String::length);
// 完了後に別の非同期タスク
CompletableFuture<String> result = future
.thenApplyAsync(data -> processData(data))
.thenCompose(processed -> saveAsync(processed));
// 完了後に副作用
future.thenAccept(data -> System.out.println("受信: " + data));
// 例外処理
future
.exceptionally(e -> "デフォルト値")
.thenAccept(System.out::println);
// 複数の Future を組み合わせる
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World");
// 両方完了してから処理
CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + " " + b);
System.out.println(combined.get()); // Hello World
// どちらか早く完了した方を使う
CompletableFuture<String> faster = f1.applyToEither(f2, s -> s);
スレッドセーフの実現
synchronized
public class Counter {
private int count = 0;
// メソッド全体をロック
public synchronized void increment() {
count++;
}
// ブロックでロック(より細かい制御)
public void incrementBlock() {
synchronized (this) {
count++;
}
}
public synchronized int getCount() {
return count;
}
}
Atomic クラス
synchronized より軽量でパフォーマンスが高い。
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // count++ と同じ(スレッドセーフ)
counter.getAndIncrement(); // 前の値を返してからインクリメント
counter.addAndGet(5); // +5
counter.compareAndSet(10, 0); // 値が10なら0に更新
// AtomicLong, AtomicBoolean, AtomicReference も同様
Lock(ReentrantLock)
private final ReentrantLock lock = new ReentrantLock();
public void process() {
lock.lock();
try {
// クリティカルセクション
doWork();
} finally {
lock.unlock(); // finally で必ず解放
}
}
// tryLock: ロックが取れない場合は待たない
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
doWork();
} finally {
lock.unlock();
}
}
よくある問題
デッドロック
// デッドロックの例
// Thread1: lockA を取ってから lockB を取ろうとする
// Thread2: lockB を取ってから lockA を取ろうとする
// → 互いに待ち続ける
// 対策: ロックの取得順序を統一する
// 常に lockA → lockB の順で取得する
可視性の問題
// volatile: 変数の変更が他スレッドにすぐ見える
private volatile boolean running = true;
// volatile がなければ、他スレッドがキャッシュした古い値を見続ける可能性がある
まとめ
| 場面 | 使うもの |
|---|---|
| シンプルなタスク実行 | ExecutorService |
| 結果が必要なタスク | Future / Callable |
| 非同期チェーン | CompletableFuture |
| カウンタ | AtomicInteger |
| 複合操作の保護 | synchronized / ReentrantLock |
| 定期実行 | ScheduledExecutorService |
マルチスレッドは「必要になったときだけ使う」のが原則です。複雑さとバグのリスクが増えるため、シングルスレッドで間に合うなら使わない方が安全です。