1. Thread クラス:Java で最初のスレッド
Java では、各実行スレッドはクラス Thread のオブジェクトで表されます。このクラスはオーケストラの指揮者のような存在で、スレッドの起動・停止・ライフサイクルを管理します。
スレッドを起動するには:
- Thread クラス(またはそのサブクラス)のオブジェクトを作成する。
- そのオブジェクトで start() メソッドを呼び出す。
実際に見てみましょう。
例 1. Thread を継承する
最も簡単な方法は、自分のクラスを Thread から継承し、そのメソッド run() をオーバーライドすることです。run() の中に書いた処理は別スレッドで実行されます。
// 継承によるシンプルなスレッド
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("スレッドからこんにちは!名前: " + getName());
}
}
public class Main {
public static void main(String[] args) {
HelloThread thread = new HelloThread(); // スレッドオブジェクトを作成
thread.start(); // スレッドを起動
System.out.println("メインスレッドは処理を終了します。");
}
}
実際には何が起きているのでしょうか?
thread.start() を呼び出すと、Java は新しいスレッドを作成し、その中でメソッド run() を起動します。一方、メインスレッド(main から始まったスレッド)は待たずに並行して処理を続けます。
重要な注意: start() と run() の直接呼び出しを混同しないでください。thread.run() と書くと新しいスレッドは作られず、単なる通常メソッドとして、呼び出したのと同じスレッドで実行されます。本当のマルチスレッドは start() から始まります。
getName() とは?
メソッド getName() はスレッド名を返します。デフォルトでは Java は "Thread-0"、"Thread-1" などの名前を付けます。デバッグに便利です。
2. Runnable インターフェース:多くの場合の最良の方法
Java は柔軟性を重んじる言語です。クラス Thread はすでに Object を継承しており、Java にはクラスの多重継承はありません。自分のクラスを Thread から継承すると、ほかのクラス(たとえば独自の Car クラスなど)を同時に継承できません。
解決策は、スレッドのロジックを Runnable インターフェースを実装した別オブジェクトに切り出すことです。これはメソッド run() を 1 つ持つインターフェースです。次に Thread のオブジェクトを作成し、作成した Runnable を渡してスレッドを起動します。
例 2. Runnable を使うクラス
// Runnable を実装するクラス
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable からこんにちは!スレッド: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable); // Runnable を Thread に渡す
thread.start();
System.out.println("メインスレッドは処理を終了します。");
}
}
ここでは HelloRunnable クラスが Runnable を実装しています。main メソッドでこのクラスのオブジェクトを作成し、コンストラクタで Thread に渡します。その後、start() を呼び出してスレッドを起動します。
特に Thread.currentThread() メソッドは重要です。これは静的メソッドで、現在実行中のスレッドのオブジェクトを取得できるため、コードがどこで実行されているかを把握できます。
なぜこちらの方がよいのか?
- 自分のクラスを任意の他のクラス(Thread だけでなく)から継承できる。
- 同じ Runnable を複数スレッドの起動に再利用できる。
- コードがより柔軟でクリーンになる。
3. 構文:無名クラスとラムダ式
Java も進化しています。Java 8 以降は、より短く、便利に書けます。
例 3. 無名クラス
一度だけ使うようなクラスのために別ファイルを作りたくないことがあります。使用箇所で直接宣言できます。これを無名クラスと呼びます。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("無名 Runnable!スレッド: " + Thread.currentThread().getName());
}
});
thread.start();
System.out.println("メインスレッドは処理を終了します。");
}
}
例 4. ラムダ式(Java 8+)
インターフェース Runnable は機能インターフェース(抽象メソッドが 1 つだけ)です。だからラムダ式が使えます。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("ラムダ式のスレッド!スレッド: " + Thread.currentThread().getName());
});
thread.start();
System.out.println("メインスレッドは処理を終了します。");
}
}
要点:
Runnable はその場で実装でき、ラムダ式はメソッド run() の実装を提供します。
4. 複数スレッドの起動
スレッドはいくつでも起動できます(実際にはマシン資源に依存します)。
例 5. 複数スレッドを起動する
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int threadNumber = i; // ラムダ用に変数のコピーを必ず作る!
Thread thread = new Thread(() -> {
System.out.println("スレッド #" + threadNumber + ": こんにちは!");
});
thread.start();
}
System.out.println("メインスレッドは処理を終了します。");
}
}
出力は毎回変わることがあります!
スレッドは並行に動作するため、どのメッセージが先に表示されるかは OS とスレッドスケジューラに依存します。
5. スレッドのライフサイクル
スレッドの振る舞いを理解することは、デバッグや正しい利用のために重要です。
主なスレッド状態
- NEW — スレッドは生成済みだが、まだ起動していない(new Thread(...))。
- RUNNABLE — スレッドは起動済み(start())。実行可能状態。
- TERMINATED — スレッドの実行が終了した(メソッド run() が終わった)。
start() と run() を呼び出すと何が起こるか
- start() — 新しいスレッドを作成し、その新しいスレッド内でメソッド run() を呼び出す。
- run() — ただのメソッド。直接呼び出すと現在のスレッドで実行され(新しいスレッドは作られない)。
例:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("スレッドからの run(): " + Thread.currentThread().getName()));
thread.run(); // メインスレッドで呼び出される!
thread.start(); // 別スレッドで呼び出される!
}
}
6. 実践:簡単なマルチスレッドアプリを作る
学習用アプリを発展させていきましょう。各スレッドが自分のメッセージを書き出す、簡単なロギングシステムを作るとします。
public class LoggerTask implements Runnable {
private final String message;
public LoggerTask(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "] ログ: " + message);
}
}
public class Main {
public static void main(String[] args) {
Thread logger1 = new Thread(new LoggerTask("システムの起動"));
Thread logger2 = new Thread(new LoggerTask("データの読み込み"));
Thread logger3 = new Thread(new LoggerTask("リクエストの処理"));
logger1.start();
logger2.start();
logger3.start();
System.out.println("メインスレッドは処理を終了します。");
}
}
何が起きている?
3 つのスレッドが並行してコンソールにメッセージを書き込みます。メッセージの順序は保証されません—これこそがマルチスレッドの本質です。
7. 役に立つ細かなポイント
表:スレッド作成方法の比較
| 方法 | 柔軟性 | 再利用性 | 推奨? |
|---|---|---|---|
| Thread の継承 | 低い | いいえ | 簡単/学習用の例に限る |
| Runnable の実装 | 高い | はい | はい |
| 無名クラス | 中程度 | いいえ | 一度きりのタスク向け |
| ラムダ式 | 高い | いいえ | はい(Java 8+) |
図:スレッド起動の流れ
+----------------------+
| main (メインスレッド)|
+----------------------+
|
v
+----------------------+
| Thread thread = ... |
+----------------------+
|
v
+----------------------+
| thread.start() | ← スレッドが「目覚める」
+----------------------+
|
v
+----------------------+
| 新しいスレッドで run()|
+----------------------+
|
v
+----------------------+
| スレッド終了 |
+----------------------+
8. スレッド起動時のよくあるミス
エラー №1: start() の代わりに run() を呼んでしまう。
初心者に非常に多いミスは、スレッドオブジェクトでメソッド run() を直接呼び出してしまうことです。この場合、新しいスレッドは作成されず、メソッドは現在(メイン)スレッドで実行されるだけです。正しくは、スレッドの起動には常に start() を使います。
エラー №2: 同じスレッドを再度起動する。
同じ Thread オブジェクトに対して start() を 2 回以上呼ぶことはできません。呼ぶと IllegalThreadStateException が発生します。もう一度タスクを実行したい場合は、新しい Thread オブジェクトを作成してください。
エラー №3: 共有変数を同期なしで変更する。
複数のスレッドが同じ変数にアクセスすると、結果が予期せぬものになることがあります(race condition)。同期については後で扱いますが、まずは注意してください。
エラー №4: スレッドの終了を考慮していない。
スレッドの終了を待つ必要がある場合は、join() メソッドを使いましょう。そうしないと、メインスレッドが子スレッドより先に終了してしまうことがあります。
エラー №5: スレッド名とクラス名を取り違える。
メソッド getName() はスレッド名を返し、クラス名ではありません。デバッグのために、コンストラクタや setName() で明示的にスレッド名を設定すると便利です。
GO TO FULL VERSION