Java入門|複数のスレッドを動かす

悟空だけでは終わらない。仲間の数だけ、処理の流れも動き出す。

前の内容では、main()とは別に新しいスレッドを1つ起動して、処理の流れが2本になる様子を見てきました。
Javaでは、この流れをさらに増やすことができます。つまり、1人の戦士だけが動くのではなく、悟空、ベジータ、ピッコロのように、それぞれが別々の任務を同時に進めるような状態を作れるのです。

ドラゴンボールの世界で考えると、悟空が修行を進めているあいだに、ベジータは重力室で鍛え、ピッコロは精神集中を続ける、という場面が思い浮かびますよね。
全員が同じタイミングで行動しているわけではありませんが、それぞれが自分の流れで動いています。Javaの複数スレッドも、それとよく似ています。

ここでは、新しいスレッドをさらにもう1つ増やしたときに、プログラム全体の流れがどう変わるのかを見ていきます。
1つの流れが2つになり、さらに3つになることで、スレッドのイメージがぐっとはっきりしてきます。

スレッドをもう1つ増やすとどうなるのか

1つのスレッドを起動したときは、

  • main()の流れ
  • 新しく起動したスレッドの流れ

の2本が動いていました。

では、さらにもう1つ start() を呼び出したらどうなるでしょうか。
答えはシンプルで、新しい処理の流れがさらに1本増えるです。

つまり、今度は次の3本になります。

流れ役割のイメージ
main()のスレッド全体の進行を担当する流れ
1つ目のスレッドベジータの修行の流れ
2つ目のスレッドピッコロの修行の流れ

ドラゴンボール風にいえば、悟空が仙豆の確認をしているあいだに、ベジータとピッコロがそれぞれ別の修行を進めるようなものです。
プログラムの中で、複数の担当者がそれぞれ仕事をしている感覚ですね。

複数スレッドのイメージ

まずは、2本ではなく3本の流れになることを図でつかんでおくと理解しやすいです。

この図が示していること

この図では、main()の流れから2回 start() を呼び出すことで、2本の新しいスレッドが増えることを表しています。

ここで大事なのは、main()が止まって別の処理に切り替わるのではなく、

  • main()も進む
  • ベジータスレッドも進む
  • ピッコロスレッドも進む

というように、処理の流れそのものが3本になることです。

この図を見ると、スレッドは1つしか作れないものではなく、必要に応じて増やせる仕組みだと分かります。

2つのスレッドを起動する基本の考え方

1つのスレッドを起動するときは、

  1. Threadクラスを継承したクラスのオブジェクトを作る
  2. start() を呼び出す

という流れでした。

複数のスレッドを動かすときも、考え方は同じです。
ただし、その手順を複数回行うだけです。

たとえば、

  • ベジータ用のオブジェクトを作って start()
  • ピッコロ用のオブジェクトを作って start()

というように書けば、別々の流れが2本起動します。

ここで大切なのは、同じクラスから作ったオブジェクトでも、それぞれ別のスレッドとして動くという点です。
同じ修行メニューを持つ戦士クラスでも、ベジータ用の個体とピッコロ用の個体は別々に行動する、というイメージです。

サンプルプログラム:複数スレッドを見てみよう

ここでは、Threadクラスを継承した SaiyanWorker クラスを使い、main()以外に2本のスレッドを起動してみます。

ファイル名:Sample2.java

class SaiyanWorker extends Thread
{
    private String name;

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

    public void run()
    {
        // それぞれの戦士が別スレッドで修行する
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "が修行メニューを進めています。");
        }
    }
}

class Sample2
{
    public static void main(String[] args)
    {
        SaiyanWorker warrior1 = new SaiyanWorker("ベジータ");
        warrior1.start();

        SaiyanWorker warrior2 = new SaiyanWorker("ピッコロ");
        warrior2.start();

        // main()側の流れ
        for (int i = 0; i < 5; i++) {
            System.out.println("悟空が修行場の準備をしています。");
        }
    }
}

このプログラムの流れ

このコードも、最初は main() から始まります。
そこでまず、ベジータ用のスレッドオブジェクトを作っています。

SaiyanWorker warrior1 = new SaiyanWorker("ベジータ");

次に、これを start() で起動します。

warrior1.start();

この時点で、ベジータの run() が別スレッドとして動き始めます。

さらに次の部分で、ピッコロ用のスレッドオブジェクトを作ります。

SaiyanWorker warrior2 = new SaiyanWorker("ピッコロ");

そして、こちらも start() で起動します。

warrior2.start();

これで、ピッコロの run() も別スレッドとして動き始めます。

最後に、main() 側でも for文を実行して、悟空のメッセージを表示します。
その結果、プログラム全体では次の3つの流れが同時に存在することになります。

流れ実行内容
main()のスレッド悟空が修行場の準備をしています。
warrior1 のスレッドベジータが修行メニューを進めています。
warrior2 のスレッドピッコロが修行メニューを進めています。

実行結果はどう見えるのか

実行結果の一例は、たとえば次のようになります。

悟空が修行場の準備をしています。
ベジータが修行メニューを進めています。
ピッコロが修行メニューを進めています。
悟空が修行場の準備をしています。
ベジータが修行メニューを進めています。
悟空が修行場の準備をしています。
ピッコロが修行メニューを進めています。
ベジータが修行メニューを進めています。
ピッコロが修行メニューを進めています。
悟空が修行場の準備をしています。

ただし、この順番は毎回同じではありません。
別の実行では、ピッコロの表示が先に多く出ることもありますし、悟空の表示が続くこともあります。

これは、3本の流れがそれぞれ独立して進んでいるからです。
つまり、どのスレッドがどのタイミングで少し先に進むかは固定されていません。

どうして3つの流れになるのか

ここはとても大事です。
main() はもともと1本のスレッドとして動いています。
そこに start() を1回呼ぶと、新しいスレッドが1本増えます。
さらにもう1回 start() を呼ぶと、もう1本増えます。

なので、

  • もとの main() の流れ
  • 1回目の start() で生まれた流れ
  • 2回目の start() で生まれた流れ

となり、合計3本になります。

ドラゴンボールの世界で考えるなら、

  • 悟空が会場準備をする
  • ベジータが重力修行をする
  • ピッコロが精神統一をする

という3人の動きが同時に存在するような状態です。
1人が終わるまで次の人が何もできないわけではなく、それぞれが自分の任務を進めます。

同じクラスから複数のスレッドを作れる

今回の例では、SaiyanWorker という同じクラスから2つのオブジェクトを作っています。
でも、その2つは同じ存在ではありません。

  • warrior1 はベジータ
  • warrior2 はピッコロ

というように、別々のオブジェクトです。
だから、同じ run() の形を持っていても、それぞれ独立して動きます。

これはとても便利な考え方です。
1つのスレッド用クラスを作っておけば、そのクラスから複数のオブジェクトを作り、それぞれを別スレッドとして起動できます。

表にすると、こうなります。

オブジェクト名前動き
warrior1ベジータベジータ用のスレッドとして動く
warrior2ピッコロピッコロ用のスレッドとして動く

つまり、クラスは共通の設計図、オブジェクトは実際に動く個別の戦士というイメージです。

図で見る複数スレッドの起動

複数スレッドの起動は、図で見ておくとさらに整理しやすいです。

この図が示していること

この図では、main() から2つの start() を呼び出すことで、2本の新しい run() の流れが始まることを表しています。

この図から分かるのは、スレッドは特別な1本だけを扱うものではなく、必要な数だけ増やせるということです。
そして、増えたスレッドはそれぞれ独立して処理を進めます。

複数スレッドで気をつけたい見方

複数のスレッドを扱うときは、出力結果を見て「順番どおりに動いていない」と感じることがあります。
でも、それは間違っているのではなく、むしろ自然な動きです。

3本の流れがあるなら、

  • 先に悟空が2回表示される
  • そのあとベジータが続く
  • 途中でピッコロが入る

というような混ざり方はいくらでもあります。

大切なのは、どの流れも独立しているという見方です。
1本の一本道ではなく、複数の道が同時に進んでいるので、細かな順番は固定されません。

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

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

ポイント内容
スレッドは複数起動できるstart() を複数回呼び出せば、その分だけ新しい流れが増える
main() も1つのスレッド新しいスレッドだけでなく、main() の流れも同時に動いている
同じクラスから複数作れる同じThread継承クラスから複数のオブジェクトを作って、それぞれ起動できる
実行順序は固定ではない出力の順番は環境やタイミングによって変わる
run() が終われば終了各スレッドは自分の run() が終わると終了する

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

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

  • 悟空は main() の流れとして全体を進める
  • ベジータは1本目の新しいスレッドとして修行する
  • ピッコロは2本目の新しいスレッドとして精神統一を行う

この3人は同じ世界の中にいますが、1人ずつ順番待ちして行動しているわけではありません。
それぞれが別々の流れで動いています。

Javaで複数スレッドを動かすというのは、まさにこの状態をコードの中で作ることです。
start() を1回呼べば仲間が1人増え、さらにもう1回呼べばまた1人増える。
その結果、プログラムの中に複数の処理の流れが生まれ、同時に仕事を進められるようになります。

ここをしっかり理解しておくと、この先の一時停止や終了待ち、Runnableインターフェイス、同期といった内容も、とても読みやすくなってきます。