並列処理の要点
内容の確認日: 2026-10-04
この分野では、スレッドの作り方と実行の仕組み、ExecutorService によるタスクの実行、複数のスレッドから同じデータを触るときの排他制御、並列処理向けのコレクション、そして Flow による非同期のデータの受け渡しが問われます。
出題項目は「Thread および Runnable を使用したスレッドの作成」「ExecutorService を使用した並列処理の実装」「synchronized を使用したスレッド制御」「java.util.concurrent パッケージの並列処理コレクションの使用 (CopyOnWriteArrayList および ConcurrentHashMap など)」「java.util.concurrent.Flow を使用したフロー制御」の 5 つです。
Thread と Runnable
スレッドで実行する処理は、Runnable (void run()) として書き、Thread に渡して start() で開始します。start() は新しいスレッドを作ってその中で run() を呼びます。run() を直接呼ぶと、新しいスレッドは作られず、呼び出したスレッドで普通のメソッドとして実行されます。
1 つの Thread オブジェクトの start() は一度しか呼べません。終了したスレッドでも、2 回目の start() は IllegalThreadStateException になります。join() は、そのスレッドが終わるまで待ちます。
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> System.out.println("run: " + Thread.currentThread().getName()), "worker"); t.run(); t.start(); t.join(); t.start(); }}java Main run: main run: worker
実行時に例外が発生: IllegalThreadStateException
ExecutorService と Callable・Future
ExecutorService は、スレッドの管理を任せてタスクを実行する仕組みです。Executors.newFixedThreadPool(n) (n 個のスレッド)・newSingleThreadExecutor() (1 つのスレッドで順番に実行)・newCachedThreadPool() などで作ります。
結果を返すタスクは Callable<V> (V call() throws Exception) で書きます。Runnable と違い、戻り値を返せ、チェック例外も投げられます。submit は Future を返し、Future.get() で結果を受け取ります (終わっていなければ待ちます)。値を返すラムダ式 () -> 6 * 7 は Callable、値を返さないラムダ式は Runnable として扱われます。
invokeAll(タスクのリスト) は、すべてのタスクが終わるまで待ってから、それぞれの Future のリストを返します。shutdown() を呼ぶと新しいタスクを受け付けなくなり (実行中・待機中のタスクは最後まで実行される)、その後の submit は RejectedExecutionException になります。
import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;public class Main { public static void main(String[] args) throws Exception { ExecutorService ex = Executors.newFixedThreadPool(2); try { Future<Integer> f = ex.submit(() -> 6 * 7); System.out.println(f.get()); List<Callable<String>> tasks = List.of(() -> "A", () -> "B", () -> "C"); for (Future<String> r : ex.invokeAll(tasks)) { System.out.print(r.get()); } System.out.println(); } finally { ex.shutdown(); } System.out.println(ex.isShutdown()); ex.submit(() -> 1); }}java Main 42 ABC true
実行時に例外が発生: RejectedExecutionException
invokeAll が返す Future のリストは、渡したタスクの順番と同じです。そのため、タスクがどの順番で終わっても ABC と出力されます。
synchronized による排他制御
n++ は「読み取り・加算・書き込み」の 3 段階の操作なので、複数のスレッドが同時に実行すると更新が失われることがあります。これを防ぐには、synchronized で同時に 1 つのスレッドしか実行できないようにするか、AtomicInteger のような不可分な操作を持つクラスを使います。
synchronized のロックとして使われるオブジェクトは次のとおりです。
・synchronized なインスタンスメソッド: this
・static synchronized なメソッド: クラス名.class
・synchronized (式) { ... } ブロック: 丸括弧に書いたオブジェクト (参照型の式でなければならない)
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;class Counter { private int n; synchronized void increment() { n++; } synchronized int get() { return n; }}public class Main { public static void main(String[] args) throws InterruptedException { ExecutorService ex = Executors.newFixedThreadPool(4); Counter counter = new Counter(); AtomicInteger atomic = new AtomicInteger(); for (int i = 0; i < 1000; i++) { ex.submit(() -> { counter.increment(); atomic.incrementAndGet(); }); } ex.shutdown(); ex.awaitTermination(10, TimeUnit.SECONDS); System.out.println(counter.get() + " " + atomic.get()); }}java Main 1000 1000
wait()・notify()・notifyAll() は、そのオブジェクトのロックを持っているスレッド (つまり、そのオブジェクトの synchronized の中) でしか呼べません。ロックを持たずに呼ぶと、実行時に IllegalMonitorStateException が発生します。
public class Main { public static void main(String[] args) { Object lock = new Object(); synchronized (lock) { lock.notify(); System.out.println("synchronized の中では呼べる"); } lock.notify(); }}java Main synchronized の中では呼べる
実行時に例外が発生: IllegalMonitorStateException
並列処理向けのコレクション
java.util.concurrent パッケージには、複数のスレッドから安全に使えるコレクションがあります。
CopyOnWriteArrayList は、変更のたびに内部の配列をコピーして書き込みます。イテレータは作った時点の配列 (スナップショット) をたどるので、拡張 for 文で回している最中に要素を追加しても ConcurrentModificationException は発生せず、追加した要素はそのループには現れません。書き込みのコストが大きい代わりに、読み取りはロックなしで速いので、読み取りが多く書き込みが少ない場面に向いています。
import java.util.List;import java.util.concurrent.CopyOnWriteArrayList;public class Main { public static void main(String[] args) { List<Integer> list = new CopyOnWriteArrayList<>(List.of(1, 2, 3)); for (Integer n : list) { if (n == 1) { list.add(4); } System.out.print(n + " "); } System.out.println(list); }}java Main 1 2 3 [1, 2, 3, 4]
ConcurrentHashMap は、merge・compute・putIfAbsent などで、1 つのキーに対する「読み取りと更新」をまとめて不可分に行えます。HashMap と違い、キーにも値にも null を入れられず、null を渡すと NullPointerException になります。
import java.util.concurrent.ConcurrentHashMap;public class Main { public static void main(String[] args) { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.merge("x", 1, Integer::sum); map.merge("x", 1, Integer::sum); map.putIfAbsent("y", 5); System.out.println(map); map.put("z", null); }}java Main {x=2, y=5}
実行時に例外が発生: NullPointerException
Flow によるフロー制御
java.util.concurrent.Flow は、データを非同期に受け渡すための 4 つのインタフェースをまとめたものです。
・Publisher: データの発行者。subscribe(Subscriber) で購読者を登録する
・Subscriber: データの購読者。onSubscribe・onNext・onError・onComplete で通知を受け取る
・Subscription: 発行者と購読者の間の購読契約。request(n)・cancel()
・Processor: Subscriber と Publisher を兼ねる中継役
購読者は Subscription.request(n) で「あと n 個まで受け取れる」と要求し、発行者は要求された数しか送りません (背圧制御)。SubmissionPublisher は Publisher の実装で、submit した要素を購読者に非同期で配信し、close() すると全要素の配信の後で onComplete を通知します。
import java.util.concurrent.CountDownLatch;import java.util.concurrent.Flow;import java.util.concurrent.SubmissionPublisher;class Printer implements Flow.Subscriber<String> { private final CountDownLatch done; private Flow.Subscription subscription; Printer(CountDownLatch done) { this.done = done; } public void onSubscribe(Flow.Subscription subscription) { this.subscription = subscription; subscription.request(1); } public void onNext(String item) { System.out.println("受信: " + item); subscription.request(1); } public void onError(Throwable t) { t.printStackTrace(); } public void onComplete() { System.out.println("完了"); done.countDown(); }}public class Main { public static void main(String[] args) throws InterruptedException { CountDownLatch done = new CountDownLatch(1); try (SubmissionPublisher<String> publisher = new SubmissionPublisher<>()) { publisher.subscribe(new Printer(done)); publisher.submit("a"); publisher.submit("b"); } done.await(); }}java Main 受信: a 受信: b 完了
配信は別のスレッドで行われるので、main は CountDownLatch で onComplete が呼ばれるまで待っています。待たずに main が終わると、受信の出力が間に合わないことがあります。
試験でよくある引っかけ
run()を直接呼ぶrun()を直接呼ぶと新しいスレッドは作られず、呼び出したスレッドで実行されます。start()を 2 回呼ぶ同じ
Threadのstart()を 2 回呼ぶとIllegalThreadStateExceptionです。shutdown()の後のsubmitshutdown()の後にsubmitするとRejectedExecutionExceptionです。実行中のタスクは最後まで実行されます。RunnableとCallable値を返し、チェック例外を投げられるのは
Callable(call()) です。Runnableのrun()はvoidです。wait・notifyの呼び出し場所そのオブジェクトのロックを持たずに呼ぶと
IllegalMonitorStateExceptionです。ConcurrentHashMapとnullConcurrentHashMapはキーにも値にもnullを入れられません (NullPointerException)。
問題で確認する
この分野の問題を、解説つきで読めます (登録不要)。
出典・参考
- Oracle University 日本「Java SE 17 Programmer II (1Z0-826-JPN) 試験」
- The Java Tutorials「Concurrency」
- Java SE 17 API ドキュメント「ExecutorService」
- Java SE 17 API ドキュメント「Flow」
コード例は、試験の対象バージョンで実際にコンパイル・実行して結果を確認しています。