Java入門|スレッドの操作と制御

止める、待つ、再開する。スレッドを操れるようになると、処理の流れはぐっと読みやすくなる。

スレッドの基本を学んだ段階では、main()とは別に新しい処理の流れを起動できることが大きなポイントでした。
けれども、実際のプログラムでは、ただ流れを増やすだけでは足りない場面がたくさんあります。

たとえば、少しだけ待ってから処理したいことがあります。
ある処理が終わるまで、次の処理を始めたくないこともあります。
複数の流れが自由に動けるからこそ、どこで止めるのか、どこで待つのか、どこで再開するのかを意識することが大切になってきます。

ドラゴンボールの世界でたとえると、戦士たちがそれぞれ勝手に動くだけでは作戦になりません。
悟空が界王拳のタイミングを合わせるために少し呼吸を整えることもあれば、ベジータの攻撃が終わるまでピッコロが待機することもあります。
つまり、戦いには流れを増やす工夫だけでなく、流れを整える工夫も必要です。

Javaのスレッドでも同じです。
スレッドは並行して動けますが、そのまま任せきりにするだけでなく、

  • 一時停止する
  • 別スレッドの終了を待つ
  • 再開のタイミングをそろえる

といった制御ができるようになると、プログラム全体の動きがぐっと理解しやすくなります。

ここでは、スレッドを一時停止する sleep() メソッドと、別のスレッドの終了を待つ join() メソッドを中心に、スレッドの操作と制御をドラゴンボールの世界観でやさしく整理していきます。

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

スレッドは start() で起動したら終わり、ではありません。
起動したあとの流れに対して、少し休ませたり、ほかのスレッドが終わるまで待たせたりできます。

この考え方はとても大事です。
なぜなら、複数のスレッドがそれぞれ好き勝手に進むだけだと、意図した順番にならなかったり、処理のタイミングが合わなかったりするからです。

ドラゴンボールで考えると、戦士が全員いっせいに突っ込むだけでは連携になりません。
悟空が元気玉をためている間は、ほかの仲間が時間を稼ぐことがあります。
逆に、仲間の技が終わるまで待ってから次の行動に移ることもあります。

Javaのスレッド操作も、それと同じように考えるとわかりやすいです。

操作役割ドラゴンボール風のイメージ
sleep()一定時間だけ処理を止める少し呼吸を整えて間を取る
join()相手のスレッドが終わるまで待つ仲間の攻撃が終わるまで待機する

スレッドを一時停止する sleep() とは

sleep() は、今動いているスレッドを一定時間だけ休ませるためのメソッドです。

たとえば sleep(1000) と書くと、そのスレッドは 1000ミリ秒、つまり1秒だけ一時停止します。
ここで大切なのは、止まるのは sleep() を呼んだそのスレッドだということです。

これは初心者のうちは少し混乱しやすいところです。
sleep() を書いたらプログラム全体が止まるように感じるかもしれませんが、そうではありません。
止まるのは、あくまでその命令を実行している流れだけです。

ドラゴンボールのたとえでいうと、ベジータが深呼吸して1秒動きを止めても、悟空まで同時に止まるわけではありません。
止まるのはベジータ自身だけです。
ほかの戦士はそのまま動けます。

サンプルプログラム:スレッド自身を一時停止する

まずは、新しく起動したスレッド側で sleep() を使う例を見てみます。
ここでは Thread を継承したクラスとして SaiyanTask を用意し、その run() の中で1秒ずつ間を空けながらメッセージを表示するようにしています。

ファイル名:Sample3.java

class SaiyanTask extends Thread
{
    private String name;

    public SaiyanTask(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)
    {
        SaiyanTask warrior1 = new SaiyanTask("ベジータ");
        warrior1.start();

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

Sample3.javaの流れ

このプログラムでは、main() の中でベジータ用のスレッドを起動しています。
そのあと main() 側では、悟空のメッセージを5回続けて表示します。

一方、ベジータ側の run() では、毎回 sleep(1000) を呼んでからメッセージを表示しています。
そのため、ベジータの流れは1回表示するごとに1秒ずつ待ちながら進みます。

つまり、このプログラムでは次のような違いが生まれます。

流れ動き方
main() のスレッドほぼ連続して表示を進める
ベジータのスレッド1秒ごとに表示する

この結果、悟空側のメッセージが先にまとまって出やすくなり、ベジータ側のメッセージはゆっくり出てくることになります。

sleep()で起きること

sleep(1000) を実行すると、そのスレッドはその場で1秒間休みます。
その間、ほかのスレッドは動くことができます。

ここでのポイントを整理すると、次のようになります。

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

ミリ秒で指定するので、

  • 1000 = 1秒
  • 500 = 0.5秒
  • 2000 = 2秒

という換算になります。

InterruptedException が必要になる理由

sleep() を使うときには、try-catch が一緒に書かれています。
これは、sleep() が InterruptedException を送出する可能性があるからです。

今の段階では、まずは

sleep() を使うときには例外処理が必要になる

と押さえておけば十分です。

コードの中では、次のような形になります。

try {
    sleep(1000);
}
catch (InterruptedException e) {}

これは、スレッドを休ませている途中で何らかの割り込みが起こる可能性に備える書き方です。
最初は少し形式的に見えるかもしれませんが、スレッド操作ではよく登場する形なので、見慣れておくことが大切です。

図で見る sleep() による一時停止

sleep() の働きは図にするととても理解しやすいです。

この図が示していること

この図では、ベジータスレッドが sleep(1000) によって1秒ずつ止まりながら進んでいる一方で、main() 側はそのまま進めることを表しています。

ここから分かるのは、sleep() がプログラム全体を止める命令ではないということです。
止まるのは、その命令を実行したスレッドだけです。
これが sleep() を理解するうえでいちばん大切な点です。

サンプルプログラム: main() 側を一時停止する

今度は逆に、main() の流れのほうを一時停止してみます。
新しく起動したスレッド側は連続して動き、main() 側だけが1秒ごとにゆっくり進むようにします。

ファイル名:Sample4.java

class SaiyanTask extends Thread
{
    private String name;

    public SaiyanTask(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)
    {
        SaiyanTask warrior1 = new SaiyanTask("ピッコロ");
        warrior1.start();

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

Sample4.javaの流れ

このプログラムでは、ピッコロの run() には sleep() がありません。
そのため、ピッコロ側のメッセージはかなり速く続けて表示されやすくなります。

一方で main() 側は、ループのたびに Thread.sleep(1000) を呼び出しています。
そのため、悟空側のメッセージは1秒ごとにゆっくり表示されます。

ここで注目したいのは、どこに sleep() を書いたかで、止まる流れが変わるということです。

sleep() を書く場所止まるスレッド
run() の中起動した別スレッド
main() の中main() のスレッド

この違いをしっかりつかめると、スレッド操作の理解がかなり進みます。

Thread.sleep() と書く理由

Sample4.java では sleep(1000) ではなく、Thread.sleep(1000) と書いています。
これは sleep() が Threadクラスのクラスメソッドだからです。

クラスメソッドは、オブジェクトを通して呼び出すというより、クラス名を付けて呼び出す形が基本です。
そのため、main() の中では Thread.sleep(1000) と書いています。

ただし、スレッドのサブクラスの中で使うときには、単に sleep(1000) と書く形もよく使われます。
学習の最初の段階では、

  • run() の中では sleep(1000) と書かれていることがある
  • main() などでは Thread.sleep(1000) と書くことが多い

くらいに見ておくとわかりやすいです。

sleep() の使いどころ

sleep() は、処理を少しゆっくり進めたいときや、一定間隔で何かをしたいときによく使われます。

たとえば、次のような場面で役立ちます。

使いどころ
一定間隔で表示したい1秒ごとにメッセージを出す
少し待ってから次へ進みたいボタン連打を防ぐために少し待つ
デモや確認を見やすくしたい出力が速すぎないように間を空ける

ドラゴンボール風にいうと、戦士の動きをあえて一定間隔にして、修行や演出のテンポを整えるようなものです。

スレッドの終了を待つ join() とは

次に出てくる join() は、sleep() とは役割が違います。
join() は、ほかのスレッドが終わるまで、自分の処理を待機させるためのメソッドです。

これはとても便利です。
なぜなら、あるスレッドの仕事が終わってからでないと、次の処理に進みたくない場面があるからです。

ドラゴンボールでたとえるなら、ピッコロの結界が完成するまで悟空が前に出ない、というような状況です。
先にやるべき仕事が終わるのを待ってから、自分の流れを再開するわけです。

サンプルプログラム:スレッドの終了を待つ

では、join() を使うプログラムを見てみましょう。
ここでは、別スレッドの修行が終わるまで main() 側が待ち、最後のメッセージを必ずそのあとに表示するようにしています。

ファイル名:Sample5.java

class SaiyanTask extends Thread
{
    private String name;

    public SaiyanTask(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)
    {
        SaiyanTask warrior1 = new SaiyanTask("ベジータ");
        warrior1.start();

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

        System.out.println("悟空が全員の修行終了を確認しました。");
    }
}

Sample5.javaの流れ

このコードでは、まずベジータのスレッドを start() で起動しています。
そのあとで、main() 側はすぐに次へ進まず、warrior1.join() を呼び出しています。

この join() によって、main() のスレッドは

ベジータのスレッドが終わるまで待機する

という状態になります。

そのため、最後の

System.out.println("悟空が全員の修行終了を確認しました。");

は、ベジータ側の run() が終わったあとにしか実行されません。

join() の意味を整理する

join() の働きを表にすると、こうなります。

書き方意味
warrior1.join()warrior1 に対応するスレッドが終わるまで待つ
warrior2.join()warrior2 に対応するスレッドが終わるまで待つ

ここで待つのは、join() を呼び出した側です。
つまり今回なら、main() 側が待っています。

この視点はとても大事です。
join() は相手を止めるメソッドではありません。
自分が相手の終了を待つためのメソッドです。

ドラゴンボール風にいえば、悟空がベジータに「止まれ」と命令しているのではなく、
悟空が「ベジータの修行が終わるまで、こちらは待っておこう」としているイメージです。

図で見る join() による待機

join() の動きも図にすると理解しやすいです。

この図が示していること

この図では、main() の流れが join() によっていったん待機し、ベジータスレッドの終了後に再開していることを表しています。

ここから分かるのは、join() が処理の順番をそろえるための道具だということです。
複数のスレッドは本来ばらばらに進みますが、join() を使うことで「この仕事が終わってから次へ進む」という流れを作れます。

sleep() と join() の違い

この2つはどちらもスレッドの流れを調整しますが、意味はまったく同じではありません。
違いをしっかり整理しておくことが大切です。

メソッド何をするかたとえ
sleep()自分を一定時間止める少し休んでから再開する
join()相手が終わるまで自分が待つ仲間の任務完了を待ってから動く

つまり、

  • sleep() は時間で待つ
  • join() は相手の終了で待つ

という違いがあります。

この区別がついていれば、コードを読むときにもかなり見通しがよくなります。

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

スレッドの操作は、長い時間がかかる処理を扱うときに特に重要です。

たとえば、

  • ネットワーク通信
  • 大きなファイルの読み書き
  • データベース処理
  • 重い計算処理

のようなものは、すぐに終わらないことがあります。

そうした処理を全部1本の流れだけでやると、ほかの処理が止まっているように見えたり、全体の動きが分かりにくくなったりします。
そこで、長い処理を別スレッドに分けたり、必要に応じて sleep() や join() で流れを整えたりするわけです。

ドラゴンボール風にいうと、巨大な元気玉をためるような重い作業を1人が担当し、そのあいだに別の仲間が補助に回るようなものです。
さらに、「ため終わるまで待つ」「少し間を取ってから動く」といった制御が入ることで、全体の作戦がうまく回ります。

ここで押さえておきたいポイント

ここまでの内容で特に大切なのは、次の点です。

ポイント内容
sleep()現在のスレッドを一定時間だけ一時停止する
sleep() の停止対象命令を書いたそのスレッド自身
Thread.sleep()Threadクラスのクラスメソッドとして呼び出す形
join()指定したスレッドが終わるまで、自分の処理を待機させる
join() の待機対象join() を呼び出した側のスレッド
例外処理sleep() と join() では InterruptedException に備える
制御の意義複数スレッドの動きを整理し、意図したタイミングに近づける

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

今回の内容をドラゴンボールの世界でまとめると、こうなります。

  • sleep() は、戦士が少し呼吸を整えて動きを止めること
  • join() は、仲間の任務が終わるまで待機すること

どちらも「流れを止める」という意味では似ていますが、理由が違います。

  • sleep() は時間を指定して休む
  • join() は相手の終了を待つ

この違いをつかめると、スレッドはただ増やすだけの仕組みではなく、タイミングを調整しながら扱う仕組みだと見えてきます。

スレッドを学び始めたばかりの段階では、まず

  • どのスレッドが止まるのか
  • なぜ止まるのか
  • 何をきっかけに再開するのか

を丁寧に見分けることが大切です。

そこがわかるようになると、複数の処理の流れをコードの中で落ち着いて追いかけられるようになります。
そして、Javaのスレッドが単なる難しい機能ではなく、複数の仕事をうまくさばくための実践的な仕組みとして見えてくるようになります。