マルチスレッド完全攻略|Java Gold並列処理の頻出ポイントと実戦例題

【図解】Java Gold試験頻出のマルチスレッドと並列処理。スレッド管理(extends Thread / implements Runnable)、ExecutorService、スレッドプール、synchronizedとReentrantLockによるスレッド安全と性能向上の両立を分かりやすく解説したアイキャッチ画像。

Java Goldの試験には、合否を大きく分ける最難関分野があります。具体的には、「並列処理(Concurrency)」のことです。

なぜなら、スレッドの複雑な挙動や、似たようなAPIの違いを問う問題が多発するからです。そのため、この分野は暗記だけでは太刀打ちできません。つまり、コードの実際の動きを正確に読み取る力が必要になります。

そこでこの記事では、複雑な並列処理の仕組みを図解を交えて丁寧に解説します。さらに、文系出身のエンジニアでも直感的に理解できるよう、専門用語はできるだけ噛み砕きました。

したがって、定番の「ひっかけパターン」を回避し、並列処理を確実な得点源にしていきましょう!


目次

1. 並列処理の全体像:3つの重要ポイント

Java Goldの試験範囲における「並列処理」は、大きく以下の3つに分類されます。

  1. ExecutorService: スレッドの生成・管理を自動化する仕組み
  2. Callable & Future: 戻り値を受け取れる並列処理
  3. Fork/Join Framework: 大きなタスクを分割して並列実行する仕組み

2. 【頻出】ExecutorServiceの使い方

スレッドを直接 new Thread() するのではなく、スレッドプールを使って管理するのが現代のJavaの主流です。

主要なメソッド一覧

メソッド内容
execute(Runnable)タスクを実行する(戻り値なし)
submit(Callable)タスクを実行し、Futureを返す(戻り値あり)
shutdown()新規タスクを拒否し、実行中のタスク完了後に終了
shutdownNow()実行中のタスクを中断して即座に終了

shutdown() を呼び出さないと、プログラムが終了せずに待機し続けてしまう問題がよく出題されます!


3. Callable vs Runnable の違い

まずは、基本となるタスクの定義についてです。スレッドに渡す処理は、主に2つのインターフェースのどちらかを使います。特に試験では、この2つの違いが単答問題やコンパイルエラーの判定でよく狙われます。

  • Runnable 戻り値がありません(void run())。また、チェック例外をスローすることもできません。
  • Callable<V> 戻り値があります(V call())。さらに、チェック例外をスローできます。

Future.get()のブロッキングに注意

Callableを実行すると、結果を受け取るためのFutureオブジェクトが返されます。しかし、ここで最も注意すべきは、結果を取り出すget()メソッドの挙動です。

Java

ExecutorService es = Executors.newSingleThreadExecutor();
Future<String> future = es.submit(() -> {
    Thread.sleep(2000); // 2秒かかる重い処理
    return "完了!";
});

// 試験で狙われる罠!
String result = future.get(); // ここでタスクが終わるまで「待機」する
System.out.println(result);

future.get()が呼ばれると、タスクが完了するまでメインスレッドは一時停止(ブロッキング)します。したがって、出力順序を問う問題では、この「待ち時間」を計算に入れ忘れないようにしましょう。

🔗 外部リンク: 詳細な仕様はOracle公式ドキュメント(Callableインターフェース)も併せて確認してください。


4. Fork/Joinフレームワークの仕組み

大量のデータを処理する際に、タスクを再帰的に分割(Fork)し、結果を統合(Join)する仕組みです。

  • RecursiveAction: 戻り値がない場合に使用
  • RecursiveTask<V>: 戻り値がある場合に使用

5. ExecutorService と 定時実行(Scheduled)

スレッドを直接 new Thread() して管理するのは大変なため、Javaではスレッドプールを管理する ExecutorService を使用します。

👑 3つの代表的なファクトリメソッド

試験コードの1行目で、どのスレッドプールが作られているかを瞬時に見極めましょう。

  1. Executors.newSingleThreadExecutor():スレッド数は1個固定。タスクは順番に処理される。
  2. Executors.newFixedThreadPool(n):スレッド数はn個固定
  3. Executors.newCachedThreadPool():必要に応じてスレッドを自動で増減する。

🔴 ScheduledExecutorService の2つのメソッドの違い

Java Gold試験頻出の定時実行API、scheduleAtFixedRate(開始時間基準)とscheduleWithFixedDelay(終了時間基準)の挙動の違いを比較したタイムライン図。タスク処理時間が長い場合の重なり方の違いを図解。

定時実行を司る ScheduledExecutorService からは、以下の2つのメソッドの「動きの違い」が非常によく出題されます。

  • scheduleAtFixedRate(task, initialDelay, period, unit)
    • 「開始時間」を基準に一定間隔で実行。タスク自体の処理時間が長くても、指定された周期(period)ごとに次のタスクを詰め込みます。
  • scheduleWithFixedDelay(task, initialDelay, delay, unit)
    • 「終了時間」を基準に一定間隔で実行。タスクが終わってから、指定された時間(delay)を空けて次のタスクを開始します。

試験対策の一言: > 「前回のタスクが終わってから〇秒後」という文脈があれば、100% scheduleWithFixedDelay が正解です。

6. 同期化の強力な味方:CyclicBarrier vs CountDownLatch

続いて、同期化クラスについてです。これらは、複数のスレッド間でタイミングを合わせるためのクラスです。この2つは役割が似ていますが、「再利用できるかどうか」という1点で明確な違いがあります。

クラス名概要再利用(リセット)試験でのキーワード
CyclicBarrier全スレッドが規定の数(バリア)に達するまで待つ可能(自動でリセットされる)await() メソッドで待機
CountDownLatchカウントが0になるまでスレッドを待たせる不可(一発使い切り)countDown()await()

👑 CyclicBarrierのコード問題パターン

Java

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("バリア解除!"));

// スレッドA, B, Cがそれぞれ以下を実行
System.out.println("準備完了");
barrier.await(); // ⚠️ 3つのスレッドがここに揃うまで全員ストップ!

3つのスレッドが await() に到達した瞬間、設定されたアクション(「バリア解除!」)が1度だけ実行され、全員が一斉に次の処理へ進みます。もしスレッドが2つしか到達しなかった場合、プログラムは永久に発進せずフリーズ(デッドロック状態)になります。スレッドの総数とバリアに設定した数を数える問題に注意してください。

7. スレッドセーフなコレクションの挙動

次に、複数のスレッドから同時にアクセスしても壊れない「スレッドセーフなコレクション」です。従来の Collections.synchronizedList() などとの違いや、拡張FOR文(イテレータ)を回した時の挙動が狙われます。

🔴 CopyOnWriteArrayList の特殊な性質

CopyOnWriteArrayListの拡張FOR文(ループ)中の挙動を図解。イテレータ取得時に作成される「Snapshot(古いデータ)」を読み込み続けるため、別スレッドで要素が追加されてもConcurrentModificationExceptionが発生せず、追加分はループに反映されない仕組みを解説。

名前の通り「書き込み時にコピーを作る」リストです。

  • 最大のメリット: データを読み込んでいる最中に、別のスレッドが要素を「追加・削除」しても ConcurrentModificationException(例外)が発生しません。

⚠️ 試験の罠(超頻出):

「要素を読み込んでいる最中のイテレータは、読み込みを開始した時点の古いスナップショットを見続ける」という性質があります。

Java

List<String> list = new CopyOnWriteArrayList<>(List.of("A", "B"));

for (String item : list) {
    System.out.print(item + " ");
    list.add("C"); // ⚠️ ループ中に要素を追加!
}
// 出力結果はどうなる?
  • 正解: A B とだけ出力されます(ループ中に C を追加しても、実行中のループには反映されません。ループが終わった後の list にはしっかり A, B, C, C と入っています)。

8. 並列ストリーム(parallelStream)の注意点

Stream APIの処理を高速化する parallelStream() ですが、「何でも早くなるわけではない」「順序がバラバラになる」というデメリット側が問題になります。

Java

List.of(1, 2, 3, 4, 5).parallelStream()
    .forEach(System.out::print); // ⚠️ 出力順序は実行するたびに変わる(例:3 1 5 4 2)

👑 試験で落とせないポイント

  • 順序を保証して並列処理したい場合は、forEach ではなく forEachOrdered を使用する。
  • 状態を持つ操作(findFirst()limit() など)は、並列ストリームにするとパフォーマンスが逆に低下する場合がある。
  • reduce(identity, accumulator, combiner) のように、並列ストリーム専用の 第3引数(combiner:結合処理) を持つメソッドの構造が問われる。

9. 本番レベル例題チェック

【第1問】ExecutorServiceの挙動

次のコードを実行した結果はどうなりますか?

Java

ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(() -> System.out.print("A"));
Future<?> f = es.submit(() -> System.out.print("B"));
System.out.print(f.get());
es.shutdown();

A. ABnull と表示される
B. AB と表示される
C. BA と表示される
D. コンパイルエラー

正解:A executeは戻り値なし、submitにRunnableを渡すと正常終了時に f.get()null を返します。

【第2問】Callableの特性

次のコードの説明として正しいものはどれですか?

Java

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 10;
};

A. チェック例外(sleep)があるため try-catch が必要
B. CallableはExceptionを投げられるため、このままコンパイルが通る
C. submit()はCallableを引数に取れない
D. 戻り値がある場合はRunnableを使うべき

正解:B Callableの call() メソッドは throws Exception が定義されているため、そのまま記述可能です。

【第3問】AtomicIntegerの安全性

AtomicInteger をマルチスレッドで1000回 incrementAndGet() した場合の結果は? A. 常に1000になる B. 1000以下になる可能性がある C. コンパイルエラー D. 実行時例外が発生する

正解:A AtomicクラスはCAS(Compare-And-Swap)という仕組みで、ロックなしでスレッド安全性を保証します。

【第4問】Fork/Joinのクラス

戻り値を返さない並列処理で継承すべきクラスは? A. RecursiveTask B. RecursiveAction C. ForkJoinPool D. ActionTask

正解:B 「Action」は戻り値なし、「Task」は戻り値ありと覚えましょう。

【第5問】CopyOnWriteArrayList

CopyOnWriteArrayList の特徴として正しいものはどれですか?

A. 読み取り操作(get)のたびに配列をコピーする
B. 書き込み操作のたびに、配列全体のコピーを作成する
C. イテレート中に要素を削除すると例外を投げる
D. スレッドセーフではない

正解:B 書き込み時にコピーを作ることで、読み取り側がロックなしで高速に動作できるようにしています。

【第6問】shutdownメソッド

shutdown() を呼び出した後の挙動はどれですか?

A. 実行中のタスクをすべて中断する
B. 新しいタスクの受け入れを停止し、投入済みのタスク完了を待つ
C. JVMを即座に終了させる
D. すべての Future.get() をキャンセルする

正解:B 「行儀よく終了を待つ」のが shutdown() です。

【第7問】Fork/Joinのメソッド

サブタスクを非同期に実行キューに追加するメソッドはどれですか? A. fork() B. join() C. split() D. compute()

正解:A fork() で分割・実行、join() で待機・取得です。

【第8問】CyclicBarrier vs CountDownLatch

CyclicBarrier の最大の特徴はどれですか?

A. 一度使うと破棄される
B. 再利用(リセット)が可能である
C. 戻り値を返すことができる
D. シングルスレッド専用である

正解:B 「Cyclic(循環)」という名前の通り、繰り返し利用できるのが特徴です。

【第9問】ConcurrentHashMap

ConcurrentHashMap がスレッド安全性を確保しつつ高速な理由はどれですか?

A. Map全体を一つのロックで保護しているから
B. ハッシュバケット(セグメント)ごとに細分化してロックするから
C. すべての値を不変オブジェクトとして扱うから
D. 読み取り専用のMapだから

正解:B 必要な部分だけをロックするため、複数のスレッドからの同時アクセスに強い設計です。

【第10問】Future.get()

Future.get() メソッドを実行した際のスレッドの状態は?

A. タスクが完了するまでブロック(待機)される
B. タスクの完了を待たずに次の行へ進む
C. タスクが未完了なら例外を投げる
D. スレッドが終了(Terminate)する

正解:A get() は結果が確定するまで呼び出し元のスレッドをブロックします。


まとめ:試験直前のチェックリスト

  • 並列処理の章で確実にスコアを稼ぐための最終チェックリストです。模擬試験などで間違えた際は、以下のどのパターンに引っかかったかを確認しましょう。
  • 左辺の型と右辺のインスタンス(メソッド)は一致しているか?
  • Future.get()によるブロッキングを考慮して出力順序を追えているか?
  • CyclicBarrierのスレッド数は足りているか?
  • スレッドセーフなコレクションの特殊な挙動を理解しているか?
  • 並列処理は見た目のコードが厳つく、最初は戸惑うかもしれません。しかし、ルール自体は非常にシンプルで厳格です。問題集の解説と照らし合わせてパターンを掴めば、本番では強力な得点源に変わります。

理解したら、問題で確認

Java Gold SE17のコード読解問題に挑戦する

この記事で学んだ内容を、Java Gold SE17向けの問題演習アプリで復習できます。 カテゴリ別問題と60問の模擬試験を、登録不要で利用できます。

Java Gold問題を解く →
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

tibiyaのアバター tibiya ITエンジニア

文系卒エンジニア(8年目)
Java / JavaScript / C# を中心に業務システム開発を経験。
Java資格・IT就活・文系エンジニアの実体験を発信しています。

コメント

コメントする

目次