SJ blog
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

マルチスレッドは「必要になったときだけ使う」のが原則です。複雑さとバグのリスクが増えるため、シングルスレッドで間に合うなら使わない方が安全です。