Java Gold SE17の並行処理問題10選|ExecutorServiceを解説

A friendly hand-drawn vector infographic guiding students for the Java Gold SE17 exam on concurrency, titled 'java Gold SE17の並行処理問題10選 | ExecutorServiceを解説'. An anthropomorphic cat character with spectacles presents detailed diagrams explaining ExecutorService concepts: Runnable vs Callable inputs, factory configurations (Fixed, Cached, Scheduled), comparisons of execute versus submit methods, waiting for Future results, and shutdown mechanisms for task control. Ten numbered panels listing additional concurrency topics like Locks and Barriers wind around the central console. Text reads: 'To quickest passing! Master concurrency'.

Java Gold SE17の勉強をしていて、

「ExecutorServiceがよくわからない」

「submitとexecuteは何が違う?」

「Future#get()はいつ処理を待つの?」

と混乱していませんか?

Java Gold SE17の並行処理は、暗記だけでは対応しにくい分野です。

実際に、私自身、Java Goldを勉強したときも、ExecutorServiceやFutureはコードを実際に追わないと理解しにくいと感じました。

さらに、試験では、単純に用語の意味を聞くのではなく、コードを読んで実行結果やコンパイルエラーを判断する問題に注意が必要です。

そこでこの記事では、Java Gold SE17対策として並行処理の問題を10問紹介します。

ExecutorService・Future・Callable・shutdown・invokeAll・parallelStreamなど、試験で迷いやすいポイントをコード問題形式で確認していきましょう。

[swell_box color=”blue” title=”この記事でわかること”]
・ExecutorServiceの基本
・executeとsubmitの違い
・RunnableとCallableの違い
・Future#get()の動作
・shutdownの意味
・invokeAllの戻り値
・parallelStreamの注意点
・Java Gold本番で並行処理問題を見抜く方法
[/swell_box]

Java Gold SE17全体の学習順に迷っている方は「Java Gold SE17対応の勉強法ロードマップ」も参考にしてください。

目次

Java Gold SE17で並行処理が難しい理由

【図解】Java Gold SE17:ExecutorService & Future 比較図解。メガネ猫のキャラクターが、execute()とsubmit()の戻り値の違い(void vs Future)、Future#get()による結果待ち、shutdown()による新規タスク受付停止(「新規受付停止」)の仕組みを比較したインフォグラフィック。

並行処理が難しい理由は、コードを上から順番に読むだけでは実行順序を判断できないことです。

通常のコードであれば、

System.out.println("A");
System.out.println("B");
System.out.println("C");

実行結果は次の通りです。

A
B
C

しかし、複数のスレッドで処理すると実行順序が保証されない場合があります。

例えば、

executor.submit(() -> System.out.println("A"));
executor.submit(() -> System.out.println("B"));

このコードを見て、

A
B

と必ず表示されると判断するのは危険です。

利用するExecutorServiceやスレッド数によって、タスクの実行順序を単純に決められないケースがあります。

Java Goldでは、

・戻り値の型
・例外を投げられるか
・処理完了を待つか
・ExecutorServiceを終了しているか
・実行順序が保証されるか

を確認する必要があります。

まずは基本問題から見ていきましょう。

ExecutorServiceやFutureの細かい仕様については、Oracle公式の「ExecutorService APIドキュメント」も確認しておきましょう。Java Goldではメソッドの戻り値やshutdown後の動作など、API仕様を正しく理解しているか問われる問題があります。

問題1:ExecutorServiceのスレッド数

次のコードを確認してください。

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        ExecutorService service =
            Executors.newFixedThreadPool(2);

        service.submit(() ->
            System.out.println("Java")
        );

        service.shutdown();
    }
}

newFixedThreadPool(2)2は何を表しているでしょうか?

A. 実行できるタスクの最大数

B. スレッドプール内のスレッド数

C. Futureの数

D. ExecutorServiceの数

正解:B. スレッドプール内のスレッド数

Executors.newFixedThreadPool(2)は、固定数のスレッドを再利用するスレッドプールを作成します。

今回のスレッド数は2です。

Executors.newFixedThreadPool(2);

ここで注意したいのは、タスクが2個しか実行できないという意味ではないこと。

10個のタスクをsubmitしても、スレッドプール内のスレッドを利用して順番に処理できます。

[swell_box color=”yellow” title=”試験ポイント”]
newFixedThreadPool(n)のnは、固定スレッドプールで利用するスレッド数です。「実行可能なタスク総数」と勘違いしないようにしましょう。
[/swell_box]

問題2:executeとsubmitの違い

次のコードを確認してください。

ExecutorService service =
    Executors.newSingleThreadExecutor();

var result = service.submit(
    () -> System.out.println("Java")
);

service.shutdown();

resultの型として正しいものはどれでしょうか?

A. Thread

B. Runnable

C. Future<?>

D. ExecutorService

正解:C. Future<?>

submit()はタスクを送信し、Futureを返します。

var result = service.submit(...);

今回のラムダ式は値を返さないRunnableとして扱われます。

submit(Runnable)の戻り値はFuture<?>です。

一方、execute()Executorインタフェースで定義されており、戻り値はvoidです。

service.execute(
    () -> System.out.println("Java")
);

そのため、次のコードはコンパイルできません。

var result = service.execute(
    () -> System.out.println("Java")
);

voidを変数へ代入しようとしているからです。

[swell_box color=”red” title=”Java Goldのひっかけ”]
submit → Futureを返す

execute → void

メソッド名だけではなく、戻り値の型を確認しましょう。
[/swell_box]

問題3:RunnableとCallableの違い

次のコードを確認してください。

ExecutorService service =
    Executors.newSingleThreadExecutor();

Future<Integer> result = service.submit(
    () -> 10 + 20
);

service.shutdown();

このコードはどうなるでしょうか?

A. コンパイルエラー

B. Futureを取得できる

C. Runnableが返される

D. 実行時例外が発生する

正解:B. Futureを取得できる

ラムダ式は次の値を返しています。

10 + 20

結果は30です。

値を返すタスクはCallableとして扱えます。

今回のイメージは次の通りです。

Callable<Integer> task = () -> 10 + 20;

Callableのcall()メソッドは値を返せます。

一方、Runnableのrun()メソッドは戻り値を持ちません。

インタフェースメソッド戻り値
Runnablerun()void
Callablecall()V

Java Goldでは、

値を返すラムダ式 → Callableの可能性

と考えると判断しやすくなります。

問題4:Future#get()の実行結果

次のコードを確認してください。

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args)
            throws Exception {

        ExecutorService service =
            Executors.newSingleThreadExecutor();

        Future<Integer> result =
            service.submit(() -> 100);

        System.out.println(result.get());

        service.shutdown();
    }
}

実行結果はどうなるでしょうか?

A. null

B. Future

C. 100

D. コンパイルエラー

正解:C. 100

Callableは100を返しています。

service.submit(() -> 100);

submitによって返されたFutureから、get()を呼び出します。

result.get();

Future#get()は、必要であればタスクの完了を待って結果を取得します。

そのため、実行結果は次の通りです。

100

ここはJava Goldで重要です。

Futureを取得した時点で結果そのものを持っているとは限りません。

get()を呼び出したとき、タスクがまだ完了していなければ完了まで待機します。

問題5:Future#get()は処理完了を待つ?

次のコードを確認してください。

ExecutorService service =
    Executors.newSingleThreadExecutor();

Future<String> result = service.submit(() -> {
    Thread.sleep(1000);
    return "Gold";
});

System.out.println(result.get());

service.shutdown();

正しい説明はどれでしょうか?

A. get()は即座にnullを返す

B. get()はタスク完了まで待つ可能性がある

C. Thread.sleepがあるためコンパイルエラー

D. CallableではThread.sleepを使えない

正解:B. get()はタスク完了まで待つ可能性がある

Future#get()を呼び出した時点でタスクが完了していなければ、結果を取得できるまで待機します。

今回のタスクでは、

Thread.sleep(1000);

が実行されます。

その後、

return "Gold";

によって文字列を返します。

結果は、

Gold

です。

また、Callableのcall()は例外をスローできます。

そのため、このラムダ式内でThread.sleep()を利用できます。

RunnableとCallableの違いでは、戻り値だけでなくチェック例外を扱えるかにも注意してください。

問題6:shutdown()を呼ぶとタスクは停止する?

次のコードを確認してください。

ExecutorService service =
    Executors.newSingleThreadExecutor();

service.submit(() ->
    System.out.println("Java")
);

service.shutdown();

shutdown()を呼び出すと、送信済みのタスクはどうなるでしょうか?

A. 必ず即座に停止する

B. 送信済みタスクも削除される

C. 送信済みタスクは実行される

D. コンパイルエラー

正解:C. 送信済みタスクは実行される

shutdown()は、すでに送信されたタスクを即座に停止するメソッドではありません。

新しいタスクの受付を停止し、送信済みのタスクは処理されます。

そのため今回のコードでは、

Java

が出力されます。

[swell_box color=”red” title=”shutdownの勘違いに注意”]
shutdown()は「すべてのタスクを今すぐ強制停止する」という意味ではありません。

新規タスクの受付を停止し、すでに送信されたタスクは実行されます。
[/swell_box]

問題7:shutdown後にsubmitするとどうなる?

次のコードを確認してください。

ExecutorService service =
    Executors.newSingleThreadExecutor();

service.shutdown();

service.submit(() ->
    System.out.println("Java")
);

実行結果として正しいものはどれでしょうか?

A. Javaと表示される

B. コンパイルエラー

C. RejectedExecutionExceptionが発生する

D. IllegalStateExceptionが発生する

正解:C. RejectedExecutionExceptionが発生する

shutdown()を呼び出したExecutorServiceは、新しいタスクを受け付けません。

その状態で、

service.submit(...);

を実行すると、タスクは拒否されます。

結果としてRejectedExecutionExceptionが発生します。

Java Goldでは、

shutdown();

の位置を必ず確認してください。

コードの最後にあるとは限りません。

shutdown後にsubmitやexecuteがないか

をチェックしましょう。

問題8:invokeAllの戻り値

次のコードを確認してください。

import java.util.*;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args)
            throws Exception {

        ExecutorService service =
            Executors.newFixedThreadPool(2);

        List<Callable<Integer>> tasks = List.of(
            () -> 10,
            () -> 20
        );

        var results = service.invokeAll(tasks);

        System.out.println(results.size());

        service.shutdown();
    }
}

実行結果はどうなるでしょうか?

A. 0

B. 1

C. 2

D. コンパイルエラー

正解:C. 2

invokeAll()はCallableタスクのコレクションを受け取ります。

今回のタスクは2つです。

() -> 10
() -> 20

invokeAll()の戻り値は、FutureのListです。

イメージは次の通り。

List<Future<Integer>>

タスクが2つあるため、

results.size()

の結果は2です。

また、invokeAll()は指定されたタスクがすべて完了するまで待機します。

submit()を複数回呼ぶコードとの違いに注意しましょう。

問題9:invokeAllの結果を取得する

次のコードを確認してください。

ExecutorService service =
    Executors.newFixedThreadPool(2);

List<Callable<Integer>> tasks = List.of(
    () -> 10,
    () -> 20
);

var results = service.invokeAll(tasks);

for (var result : results) {
    System.out.print(result.get() + " ");
}

service.shutdown();

実行結果はどうなるでしょうか?

A. 10 20

B. 20 10が必ず表示される

C. null null

D. コンパイルエラー

正解:A. 10 20

invokeAll()が返すFutureのリストは、入力したタスクの順序に対応します。

今回のタスクは、

() -> 10
() -> 20

の順番です。

そのため、Futureのリストから順番にget()すると、

10 20

と表示されます。

ここで注意したいのは、タスクの実際の完了順序と、返されるListの順序を混同しないことです。

並行処理だから必ず結果順がバラバラになるわけではありません。

利用しているAPIの仕様を確認する必要があります。

問題10:parallelStreamの出力順序

最後の問題です。

import java.util.stream.*;

public class Main {
    public static void main(String[] args) {

        Stream.of(1, 2, 3, 4)
            .parallel()
            .forEach(System.out::print);
    }
}

実行結果として正しい説明はどれでしょうか?

A. 必ず1234

B. 必ず4321

C. 出力順序は保証されない

D. parallel()はコンパイルエラー

正解:C. 出力順序は保証されない

parallel()によって並列ストリームとして処理されます。

そして今回利用している終端操作は、

forEach()

です。

並列ストリームでforEach()を利用した場合、遭遇順序は保証されません。

そのため、

1234

になる可能性もありますが、別の順番で出力される可能性もあります。

順序を維持したい場合は、

forEachOrdered(System.out::print);

を利用できます。

[swell_box color=”yellow” title=”Java Goldの見抜き方”]
parallelStreamやparallelを見つけたら、終端操作を確認しましょう。

forEach → 順序に注意

forEachOrdered → 遭遇順序を考える
[/swell_box]

Stream APIの処理順について詳しく確認したい方は「Streamのdistinct・sorted・limitの実行順序を問題で解説」も参考にしてください。

Java Gold本番で並行処理問題を見抜く5つのポイント

Java Goldの並行処理問題は、コードを最初から細かく追うと時間がかかります。

私は、次の5つを先に確認するのがおすすめです。

1. ExecutorServiceの種類を見る

まずExecutorServiceの生成部分を確認します。

Executors.newSingleThreadExecutor();

なのか、

Executors.newFixedThreadPool(2);

なのか。

SingleThreadExecutorであれば、単一のワーカースレッドでタスクが処理されます。

複数スレッドのExecutorとは動作の考え方が変わります。

2. executeかsubmitか確認する

次にメソッド名を確認します。

execute()

なら戻り値はvoid

submit()

ならFutureが返されます。

変数へ代入している場合は特に注意してください。

3. ラムダ式が値を返しているか見る

() -> System.out.println("Java")

ならRunnableとして扱える形です。

() -> 100

なら値を返しています。

Callableとして扱える可能性があります。

戻り値を見るだけでも選択肢を絞れます。

4. get・invokeAllで待機するか確認する

future.get();

を見つけたら、タスク完了待ちを考えます。

invokeAll(tasks);

も、すべてのタスク完了を待つメソッドです。

並行処理だからといって、メインスレッドが必ず先に進むわけではありません。

5. shutdownの位置を見る

最後に、

shutdown();

の位置を確認します。

shutdown後にsubmitしていれば、タスクは拒否されます。

試験ではメソッドの位置を少し入れ替えるだけで結果が変わります。

[swell_box color=”blue” title=”並行処理問題の確認順”]
① ExecutorServiceの種類
② executeかsubmitか
③ RunnableかCallableか
④ Future#get・invokeAllの有無
⑤ shutdownの位置
[/swell_box]

この順番で確認すると、コードを読みやすくなります。

実務ではExecutorServiceをどう使う?

Java Goldの勉強では、コードの実行結果やAPI仕様を細かく確認します。

一方、実務では「なぜ並行処理を使うのか」を理解することも重要です。

例えば、独立した複数の処理を実行するケースです。

Callable<String> task1 =
    () -> loadUserData();

Callable<String> task2 =
    () -> loadOrderData();

ユーザー情報の取得と注文情報の取得が独立しているなら、並行実行を検討する場合があります。

ただし、並行処理を使えば必ず高速になるわけではありません。

・共有データの競合
・例外処理
・スレッドセーフ
・デッドロック
・タスクのキャンセル
・ExecutorServiceの終了処理

など、考えることが増えます。

実務では「速そうだからparallelにする」という判断は危険です。

Java Goldで並行処理を学ぶときも、単純な暗記ではなく、

複数の処理が同時に進むことで何が起きるか

を意識すると理解しやすくなります。

Java Goldの並行処理はコード問題で覚える

Java Gold SE17の並行処理は、参考書を読むだけでは理解しにくい分野です。

今回の重要ポイントをまとめます。

[swell_box color=”blue” title=”並行処理問題の重要ポイント”]
・newFixedThreadPoolの引数はスレッド数
・executeの戻り値はvoid
・submitはFutureを返す
・Runnableは値を返さない
・Callableは値を返せる
・Future#getは必要に応じてタスク完了を待つ
・shutdown後は新しいタスクを受け付けない
・invokeAllはFutureのListを返す
・invokeAllはすべてのタスク完了を待つ
・parallel+forEachは出力順序に注意する
[/swell_box]

特に試験では、

「並行処理だから順番は全部不定」

と雑に判断しないことが重要です。

ExecutorServiceの種類、利用しているメソッド、Future#getの有無などによって結果は変わります。

まずは、

ExecutorService → submit・execute → Future → shutdown

の順番で理解しましょう。

Java Gold SE17のコード問題をもっと解きたい方は「Java Gold SE17対応の無料問題集」で腕試ししてみてください。

Java Gold全体の学習順を整理したい方は「Java Gold SE17対応の勉強法ロードマップ」も参考にしてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

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

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

コメント

コメントする

目次