【Java】JavaのThreadの基本:作り方と並行処理の仕組みを理解する

Java

こんにちは、かつコーチです。
今回はJavaの並行処理の入口となるThreadクラスを扱います。
「メソッドを2つ同時に動かすにはどうすればいいのか」という素朴な疑問から、スレッドの作り方・注意点まで整理していきます。

スレッドとは何か

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

プロセス(OSがメモリなどの資源を割り当てて実行するプログラムの単位)とスレッド(プロセスの中で実際に処理を実行する最小単位)は混同されやすい用語です。
1つのプロセスは複数のスレッドを持つことができ、それらは同じメモリ空間(ヒープ)を共有します。
Javaのプログラムを実行すると、必ずmainスレッドという1本のスレッドが自動的に動き出します。
これまで書いてきたコードはすべて、このmainスレッド1本の上で順番に処理されていたわけです。

なぜ複数スレッドが必要なのか

すべての処理を1本のスレッドで実行すると、時間のかかる処理(ファイル読み込み、ネットワーク通信など)の間、他の処理は一切進みません。
たとえばデスクトップアプリで重い計算をしている間、画面が固まって操作を受け付けなくなる現象は、これが原因であることが多いです。
複数のスレッドを使えば、重い処理をバックグラウンドで走らせながら、他の処理を並行して進められます。

Threadクラスの基本的な使い方

方法1:Threadクラスを継承する

Threadクラスを継承し、run()メソッドをオーバーライドする方法です。

public class DownloadThread extends Thread {
    @Override
    public void run() {
        System.out.println(getName() + ": ダウンロード開始");
        try {
            Thread.sleep(1000); // 処理に時間がかかることを疑似的に再現
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(getName() + ": ダウンロード完了");
    }
}

public class Main {
    public static void main(String[] args) {
        DownloadThread thread = new DownloadThread();
        thread.start(); // ここで別スレッドとして実行される
        System.out.println("mainスレッドは別の処理を続けられる");
    }
}

ここで重要なのが、run()を直接呼ぶのではなくstart()を呼ぶという点です。

つまずきやすい落とし穴:run()とstart()の違い

初めてスレッドを扱ったとき、run()を直接呼び出してしまい「並行処理になっていない」とハマったことがあります。

// ❌ Before:run()を直接呼ぶ
DownloadThread thread = new DownloadThread();
thread.run(); // ただのメソッド呼び出し。mainスレッドのまま順番に実行される
// ✅ After:start()を呼ぶ
DownloadThread thread = new DownloadThread();
thread.start(); // 新しいスレッドを立ち上げてrun()を実行させる

run()を直接呼ぶと、それはただのメソッド呼び出しであり新しいスレッドは生成されません。
start()を呼んで初めて、JVMが新しいスレッドを用意し、その中でrun()の内容を実行してくれます。
このミスはThread初学者が最も多くはまる部分だと感じています。

方法2:Runnableを実装する

継承より柔軟な方法として、Runnableインターフェースを実装しThreadのコンストラクタに渡す方法があります。

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + ": 処理実行中");
        };

        Thread thread = new Thread(task, "worker-1");
        thread.start();
    }
}

Threadはすでに他のクラスを継承している場合に使えなくなる(Javaは単一継承のため)のに対し、Runnableは他のクラスを継承しつつ実装できます。
そのため実務では継承よりもRunnableを使うケースの方が多いです。

スレッドのライフサイクルと注意点

スレッドの状態遷移

スレッドにはNEW(生成直後)、RUNNABLE(実行可能・実行中)、BLOCKED(ロック待ち)、WAITINGTIMED_WAITING(待機中)、TERMINATED(終了)という状態があります。
Thread.getState()で現在の状態を確認できます。

joinで処理の完了を待つ

start()した後、mainスレッドはそのスレッドの完了を待たずに次の処理へ進みます。
完了を待ちたい場合はjoin()を使います。

Thread thread = new Thread(() -> {
    System.out.println("別スレッドの処理");
});
thread.start();
thread.join(); // このスレッドが終わるまでmainスレッドは待機する
System.out.println("すべての処理が完了");

join()を呼ばずに集計処理などを進めてしまうと、別スレッドの結果がまだ反映されていない状態で処理が進んでしまうことがあります。

まとめ

この記事のポイント

  • スレッドはプロセス内で実行される最小単位で、mainスレッドは自動的に1本動いている
  • スレッドを開始するときはrun()ではなくstart()を呼ぶ(このミスが最もハマりやすい)
  • Threadの継承よりRunnable実装の方が柔軟で実務向き
  • 別スレッドの完了を待つにはjoin()を使う

次に読むべき記事

  • RunnableCallableの違いを詳しく知りたい方は「Runnable・Callableの違い」の記事へ
  • 実務で使うスレッド管理の方法は「ExecutorServiceでスレッドプールを使う」の記事へ

タグ: Java, 中級者向け, 並行処理

タイトルとURLをコピーしました