Java 基礎からマスターまで — 第8回
スレッドと非同期タスクを安全に実行する
本ブログ作成文は、講義を目指して製作されました。 また、作成された文章の情報も間違っている可能性がありますので、ご注意ください。
同時に複数のジョブを実行するとスループットを上げることができますが、共有状態も一緒に複雑になります。 スレッド数よりも誰が状態を変更し、完了と失敗をどのように伝えるかを最初に決める必要があります。
ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
Future<Integer> result = executor.submit(() -> loadScore());
int score = result.get();
System.out.println(score);
} finally {
executor.shutdown();
}
ジョブをExecutorServiceに送信すると、スレッドの作成と再利用がランチャーによって管理されます。 Futureのgetは完了まで現在のスレッドを待ち、作業例外も渡す。
共有状態を保護する
private final Object lock = new Object();
private int gold;
public void addGold(int amount) {
synchronized (lock) {
gold += amount;
}
}
同じ値を複数のスレッドが同時に読み書きすると、操作が失われたり中間状態が見られることがあります。 synchronized は、同じロックを使用する区間を一度に 1 つのスレッドだけを実行させる。
ロック範囲が広すぎると待ち時間が増え、複数のロックを異なる順序で取得するとデッドロックが発生することがあります。 可能であれば、変更可能な共有状態自体を減らすほうがよい。
CompletableFuture
CompletableFuture<Player> playerFuture =
CompletableFuture.supplyAsync(this::loadPlayer, executor);
CompletableFuture<List<Item>> itemFuture =
CompletableFuture.supplyAsync(this::loadItems, executor);
CompletableFuture<Void> all =
CompletableFuture.allOf(playerFuture, itemFuture);
all.join();
独立したタスクを最初に開始し、完了後のステップを組み合わせることができます。 どのランチャーでコードが動作するのか、例外をどこで処理するのかを明確にする必要があります。
並行性はコードを自動的に高速にしません。 ジョブサイズ、待ち時間、共有状態、ランチャーの終了を測定して管理する必要があります。