Java超|スレッドの流れを制御する

止める、待つ、タイミングをそろえる。スレッドの流れを制御できると、複数の処理がただ動くだけでなく、意図した順番や間合いで進められるようになります。

スレッドの基本では、main メソッドとは別に、新しい処理の流れを作れることを学びました。

Thread クラスを継承したクラスを作り、run メソッドに別スレッドで行いたい処理を書く。
そして start メソッドを呼び出すことで、main の流れとは別に、もう1本の処理の流れを動かせました。

ただし、実際のプログラムでは、スレッドを起動するだけでは足りない場面があります。

複数のスレッドが自由に動けるようになると、次に大切になるのは 流れをどう整えるか です。

たとえば、次のような場面を考えてみます。

やりたいこと必要になる制御
1秒ごとにメッセージを表示したいスレッドを一定時間だけ止める
片方の処理をゆっくり進めたいsleep で間を作る
別スレッドの処理が終わるまで待ちたいjoin で終了を待つ
最後の確認処理を必ずあとに実行したいjoin で順番をそろえる

ドラゴンボール風にたとえると、戦士本部の任務では、戦士たちが勝手に動くだけでは作戦になりません。

GokuWarrior が重力修行を進めるとき、あえて一呼吸置いてから次の動きへ移ることがあります。
VegetaWarrior が戦闘記録をまとめ終えるまで、BulmaSupport が最終確認を待つこともあります。

つまり、複数の戦士が動く任務では、次のような考え方が必要になります。

制御ドラゴンボール風のイメージ
sleep少し気を整えてから次の動きへ進む
join仲間の任務完了を待ってから次へ進む

Javaのスレッドでも同じです。

スレッドは並行して動けます。
しかし、ただ自由に動かすだけでは、出力順や処理のタイミングが分かりにくくなることがあります。

そこで、sleep メソッドや join メソッドを使って、スレッドの流れを調整します。

この記事では、スレッドを一時停止する sleep メソッドと、別スレッドの終了を待つ join メソッドを中心に、スレッドの流れを制御する考え方を整理します。

具体的な例示プログラムは Sample3.java、Sample4.java、Sample5.java を使います。

スレッドは起動したあとも操作できる

スレッドは start で起動したら終わり、というものではありません。

起動したあとのスレッドに対して、次のような制御ができます。

操作役割
sleep現在動いているスレッドを一定時間だけ一時停止する
join指定したスレッドが終わるまで、呼び出した側のスレッドを待たせる

ここで大切なのは、どちらも「流れを整えるための道具」だということです。

スレッドを複数使うと、main の流れと別スレッドの流れがそれぞれ進みます。

そのため、次のような視点が必要になります。

状況必要な考え方
出力が速すぎるsleep で少し間を空ける
先に終わってほしい処理があるjoin で終了を待つ
順番をそろえたいどのスレッドを待つのかを決める
一部だけ止めたいsleep を書く場所に注意する

ドラゴンボール風に言えば、任務中に全員が同時に飛び出すだけでは、連携が乱れることがあります。

GokuWarrior が気を整えるために一瞬止まる。
VegetaWarrior の任務完了を待ってから、BulmaSupport が最終確認を出す。

このように、流れを増やすだけでなく、流れを整えることが大切です。

sleep メソッドとは何か

sleep メソッドは、現在動いているスレッドを一定時間だけ一時停止するためのメソッドです。

たとえば、次のように書きます。

Thread.sleep(1000);

1000 はミリ秒を表します。

つまり、Thread.sleep(1000) は、現在のスレッドを1秒間止めるという意味です。

書き方意味
Thread.sleep(1000);現在のスレッドを1秒止める
Thread.sleep(500);現在のスレッドを0.5秒止める
Thread.sleep(2000);現在のスレッドを2秒止める

ここで特に大切なのは、止まるのは sleep を実行したスレッドだけ という点です。

sleep を書いたからといって、プログラム全体が止まるわけではありません。

たとえば、GokuWarrior スレッドの run の中で sleep を実行した場合、止まるのは GokuWarrior スレッドです。
main スレッドは、その間も動けます。

ドラゴンボール風に言えば、GokuWarrior が一呼吸置いて気を整えていても、BulmaSupport まで同時に止まるわけではありません。

止まるのは、その場で気を整えている GokuWarrior だけです。

sleep で止まるのは現在のスレッド

sleep を理解するときは、次の表が大切です。

sleep を書く場所一時停止するスレッド
run の中その run を実行している別スレッド
main の中main スレッド
warrior1 の run の中warrior1 のスレッド
warrior2 の run の中warrior2 のスレッド

sleep は、指定した相手を外から止める命令ではありません。

今その命令を実行しているスレッド自身が止まる と考えると分かりやすいです。

この性質を押さえると、Sample3.java と Sample4.java の違いが見えやすくなります。

図:sleep は現在のスレッドだけを一時停止する

この図が示していること

この図は、sleep がプログラム全体ではなく、sleep を実行したスレッドだけを一時停止することを表しています。

main スレッドは、BulmaSupport の見回りをそのまま進めます。

一方、GokuWarrior スレッドは、Thread.sleep(1000) の場所で1秒ずつ止まりながら進みます。

図の要素意味
mainスレッド止まらずに進む処理の流れ
GokuWarriorスレッドsleep によって一時停止する処理の流れ
砂時計アイコン一定時間だけ待つこと
Thread.sleep(1000)現在のスレッドを1秒止める処理

この図から分かることは、sleep は「すべての処理を止める命令」ではなく、「今動いているそのスレッドを休ませる命令」だということです。

sleep で今動いているスレッドの処理を止める

ここでは、別スレッド側の run メソッドの中で sleep を使う例を見ていきます。

GokuWarrior が1秒ごとに気を整えながら修行を進める一方で、main 側では BulmaSupport が修行場を見回すイメージです。

ファイル名:Sample3.java

class WarriorTask extends Thread
{
    private String name;

    public WarriorTask(String nm)
    {
        name = nm;
    }

    public void run()
    {
        for(int i = 0; i < 5; i++) {
            try {
                // このスレッドを1秒だけ休ませる
                sleep(1000);
                System.out.println(name + "が気を整えています。");
            }
            catch(InterruptedException e) {
            }
        }
    }
}

class Sample3
{
    public static void main(String[] args)
    {
        WarriorTask warrior1 = new WarriorTask("GokuWarrior");
        warrior1.start();

        for(int i = 0; i < 5; i++) {
            System.out.println("BulmaSupportが修行場を見回しています。");
        }
    }
}

Sample3.java の全体像

このプログラムでは、WarriorTask クラスが Thread クラスを継承しています。

class WarriorTask extends Thread

そのため、WarriorTask のオブジェクトは start で別スレッドとして起動できます。

main メソッドでは、GokuWarrior という名前の WarriorTask オブジェクトを作っています。

WarriorTask warrior1 = new WarriorTask("GokuWarrior");
warrior1.start();

この start によって、GokuWarrior スレッドが起動します。

一方、main 側では、BulmaSupport が修行場を見回しているメッセージを5回表示します。

for(int i = 0; i < 5; i++) {
    System.out.println("BulmaSupportが修行場を見回しています。");
}

つまり、このプログラムには2つの流れがあります。

流れ内容
main スレッドBulmaSupport が修行場を見回す
GokuWarrior スレッド1秒ずつ待ちながら気を整える

run の中で sleep を使っている

GokuWarrior スレッド側の run メソッドを見てみます。

public void run()
{
    for(int i = 0; i < 5; i++) {
        try {
            sleep(1000);
            System.out.println(name + "が気を整えています。");
        }
        catch(InterruptedException e) {
        }
    }
}

ここでは、for 文の中で毎回 sleep(1000) を実行しています。

そのため、GokuWarrior スレッドは1回メッセージを表示する前に、1秒ずつ一時停止します。

流れは次のようになります。

順番GokuWarrior スレッドの動き
1sleep(1000) で1秒待つ
2GokuWarriorが気を整えています。と表示
3もう一度 sleep(1000) で1秒待つ
4また表示
5これを5回繰り返す

一方、main 側には sleep がありません。

そのため、BulmaSupport のメッセージは比較的すばやく表示されやすくなります。

流れ動き方
main スレッドほぼ連続して表示する
GokuWarrior スレッド1秒ごとにゆっくり表示する

ドラゴンボール風に言えば、BulmaSupport は修行場を連続して見回っています。
その一方で、GokuWarrior は次の動きに入る前に一呼吸ずつ気を整えながら、ゆっくり修行を進めています。

sleep の時間指定

sleep の引数はミリ秒です。

そのため、秒に直すと次のようになります。

指定時間
10001秒
5000.5秒
20002秒
30003秒

Sample3.java では、sleep(1000) と書いています。

sleep(1000);

これは、GokuWarrior スレッドを1秒間一時停止するという意味です。

ここで main スレッドまで止まるわけではありません。

止まるのは、sleep を実行している GokuWarrior スレッドです。

InterruptedException に備える理由

sleep を使うときには、try-catch が必要になります。

try {
    sleep(1000);
    System.out.println(name + "が気を整えています。");
}
catch(InterruptedException e) {
}

これは、sleep が InterruptedException を送出する可能性があるためです。

InterruptedException は、スレッドが待機中に割り込みを受けた場合などに関係する例外です。

学習段階では、まず次のように押さえると分かりやすいです。

ポイント内容
sleep を使うInterruptedException に備える必要がある
try に書くsleep を含む処理を書く
catch に書く割り込みが起きた場合の対応を書く

ドラゴンボール風に言えば、GokuWarrior が気を整えている最中に、急な通信が届く可能性があるようなものです。

その可能性に備えるため、try-catch の形で囲んでいます。

main 側を一時停止する場合

次は、main スレッド側を一時停止する例を見ます。

Sample3.java では、run の中に sleep がありました。

そのため、GokuWarrior スレッドがゆっくり進みました。

一方、Sample4.java では、main の中に Thread.sleep(1000) を書きます。

これにより、一時停止するのは main スレッドになります。

ファイル名:Sample4.java

class WarriorTask extends Thread
{
    private String name;

    public WarriorTask(String nm)
    {
        name = nm;
    }

    public void run()
    {
        for(int i = 0; i < 5; i++) {
            System.out.println(name + "が連続して戦闘フォームを確認しています。");
        }
    }
}

class Sample4
{
    public static void main(String[] args)
    {
        WarriorTask warrior1 = new WarriorTask("VegetaWarrior");
        warrior1.start();

        for(int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
                System.out.println("BulmaSupportが作戦メモを読み返しています。");
            }
            catch(InterruptedException e) {
            }
        }
    }
}

Sample4.java の流れ

このプログラムでは、VegetaWarrior スレッドの run の中に sleep はありません。

public void run()
{
    for(int i = 0; i < 5; i++) {
        System.out.println(name + "が連続して戦闘フォームを確認しています。");
    }
}

そのため、VegetaWarrior スレッドはメッセージを比較的すばやく表示しやすくなります。

一方、main 側では、for 文の中で Thread.sleep(1000) を実行しています。

for(int i = 0; i < 5; i++) {
    try {
        Thread.sleep(1000);
        System.out.println("BulmaSupportが作戦メモを読み返しています。");
    }
    catch(InterruptedException e) {
    }
}

そのため、main スレッドは1秒ずつ待ちながら進みます。

スレッドsleep の有無動き方
VegetaWarrior スレッドなし連続して進みやすい
main スレッドあり1秒ごとに進む

ここで大切なのは、sleep をどこに書いたかで止まる流れが変わることです。

sleep を書く場所止まるスレッド
run の中その run を動かしている別スレッド
main の中main スレッド

ドラゴンボール風に言えば、Sample4.java では VegetaWarrior が戦闘フォーム確認を連続して進めます。
一方、BulmaSupport は作戦メモを1秒ごとに読み返して、ゆっくり確認しています。

Thread.sleep と書く理由

Sample4.java では、次のように書いています。

Thread.sleep(1000);

これは、sleep が Thread クラスのクラスメソッドとして用意されているためです。

main メソッドは WarriorTask クラスの中ではなく、Sample4 クラスの中にあります。

そのため、Thread.sleep(1000) のように、Thread クラス名を付けて呼び出しています。

書き方使われやすい場所
sleep(1000);Thread を継承したクラスの run の中
Thread.sleep(1000);main など、Thread クラス名を明示したい場所

どちらの場合でも、意味は「現在のスレッドを一時停止する」です。

Thread.sleep(1000) と書いたからといって、すべてのスレッドを止めるわけではありません。

止まるのは、今 Thread.sleep(1000) を実行しているスレッドです。

図:sleep を書く場所で止まる流れが変わる

この図が示していること

この図は、sleep をどこに書くかによって、一時停止するスレッドが変わることを表しています。

Sample3.java では、run の中に sleep があります。
そのため、GokuWarrior スレッドが止まります。

Sample4.java では、main の中に Thread.sleep があります。
そのため、main スレッドが止まります。

比較sleep の場所一時停止するスレッド
Sample3.javarun の中GokuWarrior スレッド
Sample4.javamain の中main スレッド

この図から分かることは、sleep は「指定した相手を止める命令」ではなく、「その命令を実行している現在のスレッドを止める命令」だということです。

sleep の使いどころ

sleep は、スレッドの流れに少し間を入れたいときに使います。

たとえば、次のような場面です。

使いどころ
一定間隔で表示したい1秒ごとにメッセージを出す
動きを見やすくしたい出力が速すぎないようにする
少し待ってから次へ進みたい処理のタイミングを調整する
デモ用の動きを作りたい画面に変化を見せながら進める

ドラゴンボール風に言えば、戦士が技と技の間に気を整えるようなものです。

速く動くだけではなく、一定の間合いを作ることで、流れが見えやすくなります。

ただし、sleep はあくまで時間で待つ方法です。

「相手の処理が終わるまで待つ」ためには、別のメソッドを使います。

それが join です。

join メソッドとは何か

join メソッドは、指定したスレッドが終わるまで、自分の処理を待機させるためのメソッドです。

たとえば、次のように書きます。

warrior1.join();

これは、warrior1 のスレッドが終わるまで、join を呼び出した側のスレッドが待つという意味です。

ここでとても大切なのは、join は相手を止めるメソッドではないという点です。

join は、自分が相手の終了を待つメソッド です。

書き方意味
warrior1.join();warrior1 のスレッドが終わるまで、呼び出した側が待つ
warrior2.join();warrior2 のスレッドが終わるまで、呼び出した側が待つ

たとえば main の中で warrior1.join() を実行した場合、待つのは main スレッドです。

warrior1 が止められるわけではありません。

warrior1 は自分の run を進めます。
main は、warrior1 の run が終わるまで待ちます。

ドラゴンボール風に言えば、BulmaSupport が GokuWarrior に「止まれ」と命令しているのではありません。

BulmaSupport が「GokuWarrior の修行が終わるまで、こちらは最終確認を待とう」としている状態です。

sleep と join の違い

sleep と join は、どちらもスレッドの流れを調整するメソッドです。

しかし、待つ理由が違います。

メソッド待つ理由再開のきっかけ
sleep指定した時間だけ待つ指定時間が過ぎる
join指定したスレッドの終了を待つ相手のスレッドが終わる

つまり、次のように整理できます。

sleep は時間で待つ。
join は相手の終了で待つ。

ドラゴンボール風に言えば、sleep は「一呼吸置いてから動く」ことです。
join は「仲間の任務完了を確認してから動く」ことです。

似ているようで、目的が違います。

join で別スレッドの終了を待つ

ここでは、main スレッドが別スレッドの終了を待つ例を見ていきます。

GokuWarrior が修行記録をまとめ終わるまで、BulmaSupport が最終確認を待つイメージです。

ファイル名:Sample5.java

class WarriorTask extends Thread
{
    private String name;

    public WarriorTask(String nm)
    {
        name = nm;
    }

    public void run()
    {
        for(int i = 0; i < 5; i++) {
            System.out.println(name + "が修行記録をまとめています。");
        }
    }
}

class Sample5
{
    public static void main(String[] args)
    {
        WarriorTask warrior1 = new WarriorTask("GokuWarrior");
        warrior1.start();

        try {
            warrior1.join();
        }
        catch(InterruptedException e) {
        }

        System.out.println("BulmaSupportがGokuWarriorの修行終了を確認しました。");
    }
}

Sample5.java の流れ

このプログラムでは、まず GokuWarrior スレッドを起動しています。

WarriorTask warrior1 = new WarriorTask("GokuWarrior");
warrior1.start();

これにより、GokuWarrior スレッドの run が動き始めます。

run の中では、修行記録をまとめているメッセージを5回表示します。

public void run()
{
    for(int i = 0; i < 5; i++) {
        System.out.println(name + "が修行記録をまとめています。");
    }
}

その後、main 側では join を呼び出しています。

warrior1.join();

この join によって、main スレッドは GokuWarrior スレッドが終わるまで待ちます。

そのため、次のメッセージは、GokuWarrior スレッドの run が終わったあとに表示されます。

System.out.println("BulmaSupportがGokuWarriorの修行終了を確認しました。");

処理の流れを整理すると、次のようになります。

順番処理
1main が GokuWarrior スレッドを作る
2warrior1.start() で GokuWarrior スレッドを起動する
3GokuWarrior スレッドが修行記録をまとめる
4main は warrior1.join() で待つ
5GokuWarrior スレッドの run が終わる
6main が再開する
7BulmaSupport が終了確認メッセージを表示する

join は順番をそろえるために使う

複数スレッドは、本来それぞれ自由に進みます。

そのため、main が先に最後のメッセージを表示してしまう可能性もあります。

しかし、join を使うと、指定したスレッドが終わるまで待てます。

Sample5.java では、main が GokuWarrior スレッドの終了を待っています。

そのため、最後の確認メッセージは必ず GokuWarrior の修行記録整理が終わったあとに表示されます。

join なしjoin あり
main が先に終了確認を出す可能性があるGokuWarrior スレッド終了後に確認を出せる
出力順が安定しにくい順番をそろえやすい
別スレッドの完了を待たない指定したスレッドの完了を待つ

ドラゴンボール風に言えば、GokuWarrior が修行記録をまとめ終える前に、BulmaSupport が「終了確認しました」と言ってしまうと不自然です。

join を使うことで、GokuWarrior の任務完了を待ってから、BulmaSupport が最終確認を行えるようになります。

join でも InterruptedException に備える

join も InterruptedException を送出する可能性があります。

そのため、sleep と同じように try-catch で囲みます。

try {
    warrior1.join();
}
catch(InterruptedException e) {
}

ここでも、待機中に割り込みが起きる可能性に備えています。

学習段階では、次のように押さえておくとよいです。

メソッド必要な例外処理
sleepInterruptedException に備える
joinInterruptedException に備える

スレッドを一時停止したり、終了を待ったりする処理では、InterruptedException がよく登場します。

図:join は相手の終了を待ってから再開する

この図が示していること

この図は、main スレッドが join によって GokuWarrior スレッドの終了を待つ流れを表しています。

main は、GokuWarrior スレッドを start で起動したあと、warrior1.join() を実行します。

その時点で、main は GokuWarrior スレッドの終了を待ちます。

GokuWarrior スレッドは止められているわけではありません。
GokuWarrior スレッドは run を最後まで進めます。

そして、GokuWarrior スレッドの run が終わると、main が再開します。

図の要素意味
warrior1.join()GokuWarrior スレッドの終了を待つ
main の待機join を呼び出した側が止まる
GokuWarrior スレッド自分の run を最後まで進める
run終了main が再開するきっかけ
終了確認join のあとに実行される処理

この図から分かることは、join は相手を止める命令ではなく、呼び出した側が相手の終了を待つ命令だということです。

sleep と join を比べて整理する

sleep と join は、どちらもスレッドの流れを一時的に止めるように見えます。

しかし、意味は違います。

メソッド止まる側待つ理由再開する条件
sleep実行している現在のスレッド指定時間だけ待つため指定時間が過ぎる
joinjoin を呼び出した側のスレッド相手の終了を待つため相手のスレッドが終わる

この違いはとても重要です。

たとえば、Thread.sleep(1000) は、現在のスレッドを1秒間止めます。

一方、warrior1.join() は、warrior1 が終わるまで、join を呼び出した側を待たせます。

書き方意味
Thread.sleep(1000);現在のスレッドが1秒待つ
warrior1.join();warrior1 が終わるまで、呼び出した側が待つ

ドラゴンボール風に言えば、sleep は自分が一呼吸置くことです。

join は、仲間の任務が終わるまで自分が待つことです。

この2つを混同しないことが、スレッド制御を理解するうえで大切です。

スレッド制御が重要になる場面

スレッド制御は、複数の処理を扱うプログラムで重要になります。

特に、次のような処理では、ただスレッドを起動するだけでなく、流れを整えることが大切です。

場面必要になる制御
一定間隔で処理したいsleep
処理の完了を待ちたいjoin
重い処理を別スレッドに分けたいstart と join
表示のタイミングを調整したいsleep
最後の集計をあとで行いたいjoin

たとえば、ファイル読み込みを別スレッドで行い、その読み込みが終わったあとに集計したい場合は、join の考え方が役立ちます。

画面に1秒ごとに進行状況を出したい場合は、sleep の考え方が役立ちます。

ドラゴンボール風に言えば、任務の進行には、待つべき場面と、少し間を置くべき場面があります。

GokuWarrior が修行の型を整えるために少し止まる。
BulmaSupport が GokuWarrior の修行完了を待ってから最終確認をする。

このように、スレッド制御は複数の流れを落ち着いて扱うための基本です。

スレッド制御で押さえておきたいこと

スレッドの流れを制御するときは、次の点を押さえておくと理解しやすくなります。

ポイント内容
sleep現在のスレッドを一定時間だけ一時停止する
sleep の指定単位ミリ秒
sleep の停止対象sleep を実行したスレッド自身
Thread.sleepThread クラスのクラスメソッドとして呼び出す形
join指定したスレッドが終わるまで待つ
join の待機対象join を呼び出した側のスレッド
InterruptedExceptionsleep や join で備える必要がある例外
制御の目的複数スレッドの動きを見やすく整える

特に大切なのは、どのスレッドが止まっているのかを見分けることです。

sleep では、現在のスレッドが止まります。
join では、join を呼び出した側が、相手の終了まで待ちます。

この違いを押さえれば、スレッド制御のコードをかなり読みやすくなります。

ドラゴンボール風に整理する

今回の内容をドラゴンボール風に整理すると、次のようになります。

Javaドラゴンボール風のイメージ
sleep戦士が一呼吸置いて動きを止める
Thread.sleep(1000)1秒だけ気を整える
run の中の sleepその戦士スレッドが一時停止する
main の中の Thread.sleep本部側の main スレッドが一時停止する
join仲間の任務完了を待つ
warrior1.join()GokuWarrior の任務完了まで、呼び出した側が待つ
InterruptedException待機中に割り込みが起きる可能性への備え

スレッドは、ただ増やすだけではありません。

増えた処理の流れを、必要に応じて止めたり、待たせたり、完了を確認してから次へ進めたりできます。

sleep は、時間で待つための方法です。
join は、相手の終了で待つための方法です。

この違いが分かると、Javaのスレッドは「勝手に動く複数の処理」ではなく、「必要に応じて流れを整えられる処理」として見えてきます。