本文へスキップ
T2R tech2rich.com
プログラミング 📚 Java入門 第12回 #Java #入門 #プログラミング初心者 #マルチスレッド #並行処理

【Java入門 第12回】マルチスレッド入門|Thread と Runnable で並行処理を理解する

Javaのマルチスレッド処理を完全初心者向けに徹底解説。Thread・Runnable の使い方、複数処理を同時に動かす書き方、synchronized によるデータ競合対策まで丁寧に解説します。

📅 公開: 2026.05.25 🔄 更新: 2026.05.25 ⏱ 読了 約7分 ✍ 管理人

第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();   // 新規受付終了(実行中のタスクは継続)
    }
}

なぜ?:

スレッドとプロセスの違い

観点プロセススレッド
単位1 アプリ全体1 アプリの中の処理単位
メモリ空間各プロセスは別々同じプロセス内で共有
切り替えコスト重い軽い
データ共有難しい簡単(変数を直接共有可・だから注意も必要)

練習問題

スレッド A1, 2, 3, 4, 5 を出力し、スレッド BA, 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 回では、以下を学びました。

次回は 「JUnit でテストを書く」 を学びます。書いたコードが本当に正しいか? を自動で確認する仕組みを学びます。お楽しみに!

関連の技術記事