第11回 でファイル入出力を学びました。今回はちょっと 「上級」 に踏み込んで、「マルチスレッド」 を学びます。
これまでのプログラムは 「上から下に 1 行ずつ実行」 されていました。 マルチスレッド を使うと、複数の処理を同時に並行で動かすことができます。重い計算と画面更新を同時にやる、複数ファイルを並行ダウンロードする、など実用シーンが豊富です。
この記事のゴール
「スレッドとは?」「Thread / Runnable の使い方」「複数処理を同時に動かす書き方」「データ競合を防ぐ synchronized」が理解できることを目指します。難しめのテーマなので、雰囲気がつかめれば OK!
スレッドとは?
プログラムを実行する 「処理の流れ」 のことを スレッド(Thread) と呼びます。
- シングルスレッド: 一本の流れで上から下に処理(これまで学んだもの)
- マルチスレッド: 複数の流れが同時並行で動く
💡 イメージ: シングルスレッドは「1 人のシェフが料理を順番に作る」、マルチスレッドは「3 人のシェフが同時にそれぞれの料理を作る」感じです。
いちばん簡単なマルチスレッド: Runnable
Runnable インターフェースを実装したクラスを Thread に渡して start() するのが、基本パターンです。
public class FirstThreadDemo {
public static void main(String[] args) {
// Runnable は「実行する処理」を表すインターフェース
// ラムダ式 () -> { ... } で簡潔に書ける(Java 8 以降)
Runnable task1 = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println("タスク1: " + i);
}
};
Runnable task2 = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println("タスク2: " + i);
}
};
// Thread にタスクを渡して、それぞれ start() で開始
new Thread(task1).start(); // 別のスレッドで task1 を実行
new Thread(task2).start(); // 別のスレッドで task2 を実行
// main スレッドの処理(これも並行で動いている)
System.out.println("main 終了");
}
}
実行結果(例):
タスク1: 1
タスク2: 1
main 終了
タスク1: 2
タスク2: 2
...
ポイント: 出力順は 毎回変わるかもしれません。これが「並行実行」の特徴です。OS のスケジューラが、空いている CPU コアにスレッドを割り当てて並行処理してくれます。
start() と run() の違い
new Thread(task1).start(); // ⭕ 新しいスレッドで実行(並行)
new Thread(task1).run(); // ❌ 普通のメソッド呼び出しと同じ(直列)
start() を必ず使うこと。run() を直接呼ぶと、ただの順次処理になります。
Thread クラスを継承するパターン
Runnable の代わりに、Thread を継承 してスレッドを作る書き方もあります。
// Thread を継承した自作スレッド
public class CountThread extends Thread {
private String name;
public CountThread(String name) {
this.name = name;
}
// 並行実行される処理(run() をオーバーライド)
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(name + ": " + i);
}
}
}
// 使う側
public class Main {
public static void main(String[] args) {
CountThread t1 = new CountThread("A");
CountThread t2 = new CountThread("B");
t1.start(); // 別スレッドで run() が呼ばれる
t2.start();
}
}
💡 どっちを使うべき?: 第 10 回で学んだ通り 「1 つしか継承できない」 ので、
Runnableを実装する方が柔軟です。最近はRunnable+ ラムダ式 がスタンダードです。
スレッドを「待つ」: join()
複数のスレッドの 完了を待ってから次の処理に進みたい場合は join() を使います。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("スレッド1: " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("スレッド2: " + i);
}
});
t1.start(); // t1 開始
t2.start(); // t2 開始
t1.join(); // t1 が終わるまで待つ
t2.join(); // t2 が終わるまで待つ
// ↑の 2 行が終わってから次が実行される
System.out.println("全スレッド完了!");
}
}
ポイント: join() は InterruptedException を投げる可能性があるので、throws InterruptedException を main に書きます。
スリープ: sleep()
スレッドを 指定ミリ秒だけ一時停止するには Thread.sleep() を使います。
public class SleepDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("開始");
Thread.sleep(2000); // 2000 ミリ秒(= 2 秒)停止
System.out.println("2 秒経った");
}
}
実用例: ポーリング(定期確認)、レート制限、タイムアウト待ちなど。
データ競合と synchronized
複数のスレッドが同じ変数を同時に読み書きすると、予期しない結果になることがあります。これを データ競合(Race Condition) と呼びます。
問題のあるコード
public class RaceDemo {
static int counter = 0; // 複数スレッドで共有する変数
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
counter++; // ← この行が「読み→足し算→書き戻し」と 3 段階に分かれている
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// 期待値: 20000、実際: 不定(15000 だったり 18000 だったり…)
System.out.println("counter = " + counter);
}
}
なぜ?: スレッド A が counter の値を読み込んだ直後にスレッド B が同じ値を読み込み、結果的に 1 回分の増加が消えてしまう ことがあるためです。
synchronized で解決
public class SafeCounterDemo {
static int counter = 0;
static final Object lock = new Object(); // ロック専用オブジェクト
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
// synchronized ブロックで囲むと、同時に 1 スレッドしか入れない
synchronized (lock) {
counter++;
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
// 必ず 20000 になる!
System.out.println("counter = " + counter);
}
}
synchronized (lock) { ... } は、同時に 1 つのスレッドしか入れない部屋を作るイメージです。
⚠ マルチスレッドの落とし穴
「数値カウンタ」のような単純な処理にも、データ競合の落とし穴があります。共有データを操作する箇所は必ず synchronized で守るのが鉄則です。実務では AtomicInteger などのスレッドセーフなクラスもよく使われます。
実用パターン: ExecutorService
実務では Thread を直接 new する よりも、ExecutorService(エグゼキューターサービス) で スレッドプール を使うのが定石です。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
public static void main(String[] args) {
// スレッドプール作成(同時に 3 スレッドまで動かす)
ExecutorService executor = Executors.newFixedThreadPool(3);
// 5 個のタスクを投入(プールが順次実行)
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("タスク " + taskId + " を処理中(" + Thread.currentThread().getName() + ")");
});
}
executor.shutdown(); // 新規受付終了(実行中のタスクは継続)
}
}
なぜ?:
new Thread()は毎回作成・破棄のコストがかかる- スレッドプール は数本の Thread を 使い回すので効率的
- 実務では Spring・サーブレットなど、ほぼ全てがスレッドプール経由で動いています
スレッドとプロセスの違い
| 観点 | プロセス | スレッド |
|---|---|---|
| 単位 | 1 アプリ全体 | 1 アプリの中の処理単位 |
| メモリ空間 | 各プロセスは別々 | 同じプロセス内で共有 |
| 切り替えコスト | 重い | 軽い |
| データ共有 | 難しい | 簡単(変数を直接共有可・だから注意も必要) |
練習問題
スレッド A が 1, 2, 3, 4, 5 を出力し、スレッド B が A, B, C, D, E を出力する、2 つのスレッドを並行実行するプログラムを書いてください。両方が終わってから 「完了!」と表示してください。
解答例
public class ParallelPrint {
public static void main(String[] args) throws InterruptedException {
// スレッド A: 数字を出力
Thread numberThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("数字: " + i);
try {
Thread.sleep(100); // 少しゆっくり(視覚的にわかりやすく)
} catch (InterruptedException e) {
// ラムダ内では throws できないので try-catch で受ける
}
}
});
// スレッド B: アルファベットを出力
Thread letterThread = new Thread(() -> {
for (char c = 'A'; c <= 'E'; c++) {
System.out.println("文字: " + c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// 同上
}
}
});
numberThread.start(); // A 開始
letterThread.start(); // B 開始
numberThread.join(); // A 完了まで待つ
letterThread.join(); // B 完了まで待つ
System.out.println("完了!");
}
}
実行結果(例 — 順序は実行ごとに変わります):
数字: 1
文字: A
数字: 2
文字: B
数字: 3
文字: C
数字: 4
文字: D
数字: 5
文字: E
完了!
まとめ
第 12 回では、以下を学びました。
- ✅ スレッド は「処理の流れ」を表す単位
- ✅
Runnable+ ラムダ が現代的な書き方 - ✅
start()で並行実行開始(run()を直接呼ばない!) - ✅
join()でスレッドの完了を待つ - ✅
Thread.sleep(ミリ秒)で一時停止 - ✅ データ競合 は
synchronizedで防ぐ - ✅ 実務では
ExecutorServiceでスレッドプール運用が定石
次回は 「JUnit でテストを書く」 を学びます。書いたコードが本当に正しいか? を自動で確認する仕組みを学びます。お楽しみに!