マルチスレッドプログラミング

スレッド

プログラムがどうやって実行されるか考える場合、考え方の一つに「ロボットがプログラムの命令を順次解釈していく」というものがある。


このロボットはプログラム内の各命令を順番に調べていき、その命令を正確に解釈することによってプログラムを実行する。

if-else などの分岐が起こった場合や、for, while などで繰り返しを行う場合、メソッドを呼び出す場合も同様である。



ロボットの通り道を矢印で表したが、この矢印はプログラムの実行中、一本の糸のように途切れることなく続いている。if, for などの繰り返し命令、メソッド呼び出しなどの命令を解釈する場合にも、ロボットがたどった道は一本の制御の流れで表すことができる。

このような一連の制御の流れを、スレッド (thread of control) と呼ぶ。

マルチスレッドプログラミング

これまでのプログラムは、基本的にスレッドが一本だけであった。


java.lang.Thread というクラスを使用すると、今まで一本だけであったスレッドをプログラムから自由に作成できるようになる。

package j2.lesson13.example;

public class SayHelloThread extends Thread {
    public void run() {
        // "Hello, world!" と 10 回表示する
        for (int i = 1; i <= 10; i++) {
            System.out.println("Hello, world! - " + i);
        }
    }
}

上記のように、java.lang.Thread クラスを継承した新しいクラスを作成し、run() というメソッドをオーバーライドしてプログラムを書く。

package j2.lesson13.example;

public class SayHello2 {

    public static void main(String[] args) {
        SayHelloThread thread = new SayHelloThread();
        
        // 新しいスレッドで SayHelloThread.run() を実行する
        thread.start();

        // "こんにちは、世界!" と 10 回表示する
        for (int i = 1; i <= 10; i++) {
            System.out.println("こんにちは、世界! - " + i);
        }
    }
}

そして、Thread クラスには start() というメソッドが用意されており、このメソッドを呼び出すと Thread クラスの run() メソッドが 新しいスレッド上で実行される

プログラムで新しくスレッドを作成する場合、これまでのような言い方をするならば「新しくプログラムを実行するロボットを作成する 」ということになる。


上記の例では SayHelloThread が run() メソッドをオーバーライドし、"Hello, world!" と10回表示するようなプログラムを書いているので、新しく作成されたロボットは、元からいたロボットと平行しながら "Hello, world!" と10回表示する。

こんにちは、世界! - 1
Hello, world! - 1
こんにちは、世界! - 2
Hello, world! - 2
Hello, world! - 3
Hello, world! - 4
Hello, world! - 5
Hello, world! - 6
Hello, world! - 7
Hello, world! - 8
Hello, world! - 9
こんにちは、世界! - 3
こんにちは、世界! - 4
こんにちは、世界! - 5
こんにちは、世界! - 6
こんにちは、世界! - 7
Hello, world! - 10
こんにちは、世界! - 8
こんにちは、世界! - 9
こんにちは、世界! - 10

上記のように、元からいたロボットが "こんにちは、世界!" と10回表示し、それと平行して新しいロボットが "Hello, world!" と10回表示しようとするため、2つの出力が混ざってコンソールに表示されている (実行するたびに結果が異なる場合がある)。

このように、二つ以上のスレッドを用いてプログラミングを行うことを、マルチスレッドプログラミングと呼ぶ。

java.lang.Thread

Java のプログラム内で新しくスレッドを作成し、そのスレッド内で別の仕事をさせたい場合には java.lang.Thread を使うのが一番簡単である。

使い方は、上記クラスを継承したクラスを作成し、run() メソッドをオーバーライドして新しいスレッド内での仕事内容を書き、インスタンス化して start() メソッドを呼び出せばよい。

package j2.lesson13.example;

public class ThreadSample extends Thread {
    
    private final String printed;
    
    public ThreadSample(String printed) {
        this.printed = printed;
    }
    
    public void run() {
        // コンストラクタに指定された引数を10回表示する
        for (int i = 1; i <= 10; i++) {
            System.out.println(i + ":" + this.printed);
        }
    }

    public static void main(String[] args) {
        ThreadSample thread1 = new ThreadSample("ほげ");
        ThreadSample thread2 = new ThreadSample("ふー");
        thread1.start();
        thread2.start();
    }
}

Thread インスタンスは一度しか start() メソッドを呼び出すことができないことに注意する必要がある。例えば、上記の例で "ほげ" という文字列を 20 回表示したい場合に、次のように書いてはならない

package j2.lesson13.example;

public class ThreadSample2 extends Thread {
    
    private final String printed;
    
    public ThreadSample2(String printed) {
        this.printed = printed;
    }
    
    public void run() {
        // コンストラクタに指定された引数を10回表示する
        for (int i = 1; i <= 10; i++) {
            System.out.println(i + ":" + this.printed);
        }
    }

    public static void main(String[] args) {
        ThreadSample2 thread = new ThreadSample2("ほげ");
        // 1 回目は OK
        thread.start();
        // 2 回目は ERROR
        thread.start(); // (23行目)
    }
}

上記プログラムを実行すると、スレッドの状態が異常であることを表す java.lang.IllegalThreadStateException がスローされる。

Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:571)
	at j2.lesson13.example.ThreadSample2.main(ThreadSample2.java:23)1:ほげ
2:ほげ
3:ほげ
4:ほげ
5:ほげ
6:ほげ
7:ほげ
8:ほげ

9:ほげ
10:ほげ

同じ内容のスレッドを2度開始させたければ、次のように別々のインスタンスを作成し、それぞれに対して start() メソッドを呼び出さなければならない。

public static void main(String[] args) {
    ThreadSample2 thread1 = new ThreadSample2("ほげ");
    thread1.start();
    ThreadSample2 thread2 = new ThreadSample2("ほげ");
    thread2.start();
}

java.lang.Runnable

新しいスレッドを作って別の作業をさせたい場合、java.lang.Runnable インターフェースを利用すると再利用性の高いプログラムが書ける。

java.lang.Runnable インターフェースは、run() というメソッドを定義するだけのインターフェースである。

package j2.lesson13.example;

public class SayHelloTask implements Runnable {

    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("Hello, world! - " + i);
        }
    }
}

スレッドを継承する場合とほとんど同じ見た目であるが、extends Thread の代わりに implements Runnable となっている。

今回の SayHelloTask は、java.lang.Thread を継承していないため、start() メソッドを直接呼び出すことはできない。そこで、Thread を生成する際にコンストラクタの引数に SayHelloTask のインスタンスを渡すことによって、Thread から このクラスの run() メソッドを呼び出してもらう。

package j2.lesson13.example;

public class SayHelloTaskRunner {

    public static void main(String[] args) {
        // Runnable を継承したクラスをインスタンス化し
        SayHelloTask task = new SayHelloTask();
        
        // Thread をインスタンス化する際に引数に渡す
        Thread thread = new Thread(task);
        
        // Thread の start() メソッドを呼び出すと、task.run() が呼ばれる
        thread.start();
    }
}

上記のように書くと、次のような利点がある。

つまり、Runnable のみを実装することで、プログラムの再利用性を向上させることができる。使い捨てのプログラムの場合は Thread を継承すれば十分であるが、通常は Runnable を実装するほうがよいであろう。

スレッドの制御

新しいスレッドを作成してプログラムを実行させると、そのスレッドは自動的にプログラムを実行する。この実行を細かく操作するためのメソッドが java.lang.Thread に用意されている。

例えば、次のようなことができる。

スレッドの休止

スレッドの制御で最も単純なものは、一定時間スレッドの休止を行う Thread.sleep というメソッドである。

package j2.lesson13.example;

public class ThreadSleep {
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("5000ミリ秒だけ休止");
        Thread.sleep(5000);
        System.out.println("休止終了");
    }
}

このプログラムを実行すると、「5000ミリ秒だけ休止」と表示され、その約 5 秒後 (5000 ミリ秒後) に「休止終了」と表示されてプログラムが終了する。

Thread.sleep メソッドは java.lang.Thread クラスで宣言されているクラスメソッドで、引数に休止する時間をミリ秒で指定することによって、現在のスレッドを指定した時間だけ休止する。自分以外のスレッドを休止させることはできないので注意する必要がある。

main メソッドに指定されている「throws InterruptedException」は、java.lang.InterruptedException というスレッドへの割り込みが発生した際にスローされる例外に対処するためにつけられている。

これは Thread.sleep などがスローする例外で、長時間スレッドが停止する際に、何らかの理由で休止状態に割り込んでその休止状態をキャンセルしたい場合などに使用される。詳しくは後ほど述べる。

終了の待機

スレッドを使って、100000回だけカウントするプログラムを考える。

package j2.lesson13.example;

public class Counter {
    
    private int count;
    
    public Counter() {
        this.count = 0;
    }
    
    public void countUp() {
        this.count += 1;
    }
    
    public int get() {
        return this.count;
    }
}

上記のクラスは、カウントした回数を保持しておくためのクラスである。countUp メソッドによってインスタンスが持つ値を1だけ増加させ、get メソッドによってインスタンスが持つ値を取得できる。

package j2.lesson13.example;

public class CountUpTask implements Runnable {

    private Counter counter;

    public CountUpTask(Counter c) {
        this.counter = c;
    }
    
    // run メソッドで 100000 回だけカウントアップする
    public void run() {
        for (int i = 0; i < 100000; i++) {
            this.counter.countUp();
        }
    }
}

そして、上記は Counter インスタンスの countUp() メソッドを 100000 回呼び出す。これによって、Counter インスタンスが持つ値は 100000 だけ増加するはずである。このクラスは java.lang.Runnable インターフェースを実装しているので、新しいスレッド内で上記の操作を行える。

CountUpTask を用いてカウンタの値を増加させる例を示す。

package j2.lesson13.example;

public class CountUpTest1 {

    public static void main(String[] args) {
        // カウンタを作成する
        Counter counter = new Counter();
        
        // 100000 カウントするためのインスタンスを作成する
        CountUpTask task = new CountUpTask(counter);
        
        // 100000 カウントするスレッドを作成する
        Thread thread = new Thread(task);
        
        // カウントを開始する
        thread.start();
        
        // カウンタの値を表示する
        System.out.println(counter.get());
    }
}

上記のプログラムは、カウンタとそれを増加させるインスタンスを作成し、新しいスレッド内でカウンタの値を増加させ、最後にカウンタの値を表示している。これを実行すると、次のように表示される。

0

100000 になってほしいところが、実際には 0 と表示されてしまった (実際には、0 から 100000 の間で変化する場合もある)。


これは、上記のように「新しいスレッドが100000回だけ countUp メソッドを呼び出す」という操作を行う前に、元のスレッドがカウンタの値を取得して表示しようとしてしまっているからである。

この問題を解決するには、新しいスレッドの作業が終わるまで待機して、作業が終了したらカウンタの値を表示すればよい。

package j2.lesson13.example;

public class CountUpTest2 {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        CountUpTask task = new CountUpTask(counter);
        Thread thread = new Thread(task);
        
        // カウントを開始する
        thread.start();
        
        // カウントアップが終了するまで待機
        thread.join();
        
        // カウンタの値を表示する
        System.out.println(counter.get());
    }
}

Thread クラスの join() というインスタンスメソッドを呼び出すと、そのインスタンスが表すスレッドが終了するまで (run メソッドを抜けるまで) 待機する。スレッドが終了したら join メソッドから帰ってきて、プログラムの続きを実行する。


join は「ロボットが他のロボットと合流する」という意味にも取れる。あるスレッドが他のスレッドの join メソッドを呼び出すと、join メソッドを呼び出されたスレッドはプログラムが完了したら join を呼び出したスレッドに合流する。合流が完了するまで、join を呼び出した側は待つことになる。

このプログラムでは、カウントアップが終わるまで待機してからカウンタの値を表示するため、必ず 100000 と表示されるようになる。

なお、Thread.sleep と同様に join メソッドも java.lang.InterruptedException をスローする可能性がある。

スレッドセーフ

先ほど作成したカウントアップのプログラムを、二つ以上のスレッドで動作させる例を考える。

package j2.lesson13.example;

public class MultiThreadedCountUp {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        // カウントアップするスレッドを2つ作る
        CountUpTask countUp = new CountUpTask(counter);
        Thread t1 = new Thread(countUp);
        Thread t2 = new Thread(countUp);
        
        // 合計200000回カウントアップする
        t1.start();
        t2.start();
        
        // カウントアップが終わるまで待つ
        t1.join();
        t2.join();
        
        // 結果を表示
        System.out.println(counter.get());
    }
}

これを実行しても、「200000」という値は表示されないことが多い。代わりに、「188109」などの 200000 以下の値が表示される。

この理由は、メソッド countUp の動作にある。

public void countUp() {
    this.count += 1;
}

非常に簡単なメソッドであるが、実はこのメソッドの中では実際には次のようなことが行われている。

  1. count の値を読み出す
  2. 読み出した値に 1 を足す
  3. 足した結果を count に保存する

これを 2 つのスレッドで同時に実行した場合、次のような動作をする場合がある。


これは、Counter クラスの count インスタンスフィールドが 1500 であった際に、同時に 2 つのスレッドから countUp メソッドが呼び出された場合について書いている。

Counter が 1500 を表しているときに、countUp メソッドを 2 回呼び出せばその値は 1502 になるはずである。しかし、インスタンスフィールドの更新中に他のスレッドも更新しようとした場合、カウンタの値が不正になる場合がある。

先の例では 同時に 1500 という値をそれぞれのスレッドが読み出してしまい、それぞれが 1501 という値を保存したために 1502 になっていない。

このような問題を回避するには、countUp メソッドを二つ以上のスレッドで同時に実行できないようにする という方法が考えられる。

このような処理を施したい場合、対象のメソッドに synchronized というアクセス修飾子をつければよい。

package j2.lesson13.example;

public class Counter {
    
    private int count;
    
    public Counter() {
        this.count = 0;
    }
    
    public synchronized void countUp() {
        this.count += 1;
    }
    
    public int get() {
        return this.count;
    }
}

このように書くと、countUp メソッドは必ず一つのスレッドだけで実行されるようになり、先ほどの 200000 回カウントアップするプログラムは正しく動作する。


より正確に書くと、synchronized で修飾されたメソッドを実行しようとすると、this オブジェクトの monitor (モニタ) に入れたらメソッドを実行するようになる。

それぞれのオブジェクトにはスレッドが一つだけ入れる「monitor」と呼ばれる領域が存在し、synchronized で修飾されたメソッドを実行する際には、そのモニタに入った状態で処理を行い、メソッドが終了した際にそのモニタを抜ける。

誰か他のスレッドがモニタに入っていた場合には、他のスレッドは synchronized で宣言されたメソッドを実行することはできない。モニタが開放されるまで待機することになる。


同じクラス内にいくつも synchronized で修飾されたインスタンスメソッドを定義した場合、それらは全て同じモニタを使用することになる。そのため、それらのメソッドは全体を通して二つ以上のスレッドで同時に実行できない。

モニタはインスタンスごとに存在するため、別のインスタンスの synchronized メソッドを実行する分には問題ない。

まとめると、synchronized を使用してモニタに入らなければならない場面は、二つ以上のスレッドで同じデータを扱う場合である。このような場合は、そのデータを操作するメソッド全てに synchronized をつければよい。このように、複数のスレッドからのアクセスを考慮したプログラムをスレッドセーフなプログラムと呼ぶ (逆は「スレッドアンセーフ」)。

スレッドへの割り込み

Thread.sleep や Thread.join メソッドを呼び出すと、現在のスレッドの実行を休止する。この際に java.lang.InterruptedException という例外をスローする可能性があるが、これは休止状態に対して割り込み要求が掛かった際 - つまり、「これ以上休止するな」という命令を受け取った際にスローされる例外である。
この例外をスローさせるには、Thread クラスにある interrupt() というインスタンスメソッドを呼び出せばよい。このメソッドが呼び出されたスレッドが休止状態であった場合や、以後に休止状態になろうとした場合には java.lang.InterruptedException がスローされる。

例えば、1秒休止して「"Hello, world!"」と表示するだけのプログラム LazyTask クラスを用意する。これは新しいスレッドで実行させたいため、Runnable インターフェースを実装する。

package j2.lesson13.example;

public class LazyTask implements Runnable {

    public void run() {
        try {
            for (int i = 1; i <= 10; i++) {
                // 毎回 1 秒ずつ休止する
                Thread.sleep(1000);

                System.out.println("Hello, world! - " + i);
            }
        }
        catch (InterruptedException e) {
            System.out.println("interrupted");
        }
    }
}

そして、上記を実行するプログラムを用意する。

package j2.lesson13.example;

public class ThreadInterrupt {

    public static void main(String[] args) throws InterruptedException {
        LazyTask task = new LazyTask();
        Thread lazyThread = new Thread(task);
        
        // スレッドを開始する
        lazyThread.start();
        
        // 5秒間だけ待つ
        Thread.sleep(5000);
        
        // 待ちきれないので終了
        lazyThread.interrupt();
    }
}

ただし、LazyTask クラスのインスタンスを新しいスレッドで実行した後、5 秒だけ待ってスレッドに割り込みを要求する


上記のプログラムを実行すると、下記のように表示される ("Hello, world! - 5" は表示されないこともある)。

Hello, world! - 1
Hello, world! - 2
Hello, world! - 3
Hello, world! - 4
Hello, world! - 5
interrupted

「スレッドに割り込む」という機構は、スレッドを終了させるときによく使用される。そのため、スローされる InterruptedException をキャッチした場合には速やかにそのスレッドで実行されているプログラムを終了させることが望ましい。

サンプルプログラムで InterruptedException を無視するものをよく見かけるが、このようなことはできるだけしないほうが懸命である。

package j2.lesson13.example;

public class IgnoreInterrupted implements Runnable {

    public void run() {
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                // 例外を無視
            }
            System.out.println("Hello, world! - " + i);
        }
    }
}

実行の一時停止

少し難しいのが、スレッドを一時的に停止するというプログラムである。

package j2.lesson13.example;

public class BusyLoop implements Runnable {

    private boolean ok;
    
    public BusyLoop() {
        super();
        this.ok = false;
    }

    public void run() {
        // 何らかの処理
        
        // this.ok が false の間はここで止まる
        while (this.ok == false) {
            // 何もしない
        }
        
        // this.ok が true になると処理を行う
        
        // 何らかの処理
    }
    
    // this.ok を true にするメソッド
    public void ok() {
        this.ok = true;
    }
}

上記のようなプログラムを書けば、確かにスレッドを一時的に停止させることができる。しかし、このように書くと下記の部分でスレッドがCPUリソースを大量に使用してしまい、他のプログラムの実行を妨げてしまう (this.okの内容を読み出して、それがfalseかどうかをしらべる、ということを時間の限り実行し続ける)。

while (this.ok == false) {
    // 何もしない
}

このような何もしない無限ループのことをビジーループ (busy loop)と呼ぶ。

Thread.sleep を使用すればこの問題は少し緩和できる。

while (this.ok == false) {
    Thread.sleep(100);
}

上記のように書くと (InterruptedException は省略している)、CPUリソースを浪費することはなくなるが、今度は 1/10 秒単位でしか動作しないプログラムになってしまう。

最近のコンピュータは、1/1000000000 秒未満という非常に短い単位で動作する。そのため、Thread.sleep を用いてもっと短い単位でプログラムを動作させるには、次のように書くことになる。

while (this.ok == false) {
    Thread.sleep(1);
}

今度は、1/1000 秒単位で動作するようになったものの、1 秒間に 1000 回以上の Thread.sleep メソッドを使用することになる。Thread.sleep はそれなりにCPUリソースを使用する命令で、これでもまだ無駄が多い。

Java でこのようなプログラムを書く場合、java.lang.Object クラスの wait, notifyAll という 2 つのインスタンスメソッドを使用すると便利である。

java.lang.Object クラスは、全て区のクラスが暗黙のうちに継承している (extends を書かないと自動的に継承する)。そのため、これらのメソッドはどのクラスでも使用することができる。

ただし、これらのメソッドは概念と使用法が少し難しい。例を挙げて示す。

package j2.lesson13.example;

public class WaitObject {

    // このオブジェクトを使って待機状態に入る
    public synchronized void suspend() throws InterruptedException {
        this.wait();
    }
    
    // このオブジェクトを使って待機しているスレッドを全て起こす
    public synchronized void resume() {
        this.notifyAll();
    }
}

this.wait() メソッドを実行すると、そのスレッドはプログラムの実行をそこで停止し、このオブジェクトのウェイトセットに入る。なお、このメソッドもプログラムを一時的に休止させる類のものであるため、Thread.sleep や Thread.join メソッドと同様に java.lang.InterruptedException をスローする可能性がある。

this.wait() で停止状態になったスレッドは、他のスレッドが this.notifyAll() メソッドを実行するまでウェイトセットで待機する。他のスレッドが notifyAll() メソッドを呼び出すと、このオブジェクトのウェイトセットに入っている全てのスレッドはウェイトセットから抜け、停止していたプログラムの実行を再開する

wait, notifyAll 共、必ず this のモニタに入った状態 (synchronized メソッドを実行した状態) でなければ実行できないことに注意する必要がある。これを守らないと、java.lang.IllegalMonitorStateException がスローされる。

上記 2 つのメソッド wait と notifyAll を使用することによって、スレッドの停止と再開を実現することができる。

まずは wait メソッドの使用例を挙げる。

package j2.lesson13.example;

public class WaitTask implements Runnable {
    
    private WaitObject waitObject;

    public WaitTask(WaitObject wait) {
        this.waitObject = wait;
    }

    public void run() {
        System.out.println("wait 開始");
        try {
            // 待機状態へ
            this.waitObject.suspend();
            System.out.println("wait 終了");
        }
        catch (InterruptedException e) {
            System.out.println("interrupted");
        }
    }
}

上記のプログラムは、「wait 開始」と表示した後に WaitObject クラスのインスタンスメソッド suspend() を呼び出し、スレッドの実行を一時的に停止している。

// WaitObject.suspend()
public synchronized void suspend() throws InterruptedException {
    this.wait();
}

WaitTask クラスの run() メソッドを別のスレッドで実行する例を示す。

public static void main(String[] args) {
    // 待機用のオブジェクト
    WaitObject wait = new WaitObject();
    
    // 待機するだけのプログラム
    WaitTask task = new WaitTask(wait);
    Thread thread = new Thread(task);
    thread.start();
}

このプログラムを実行すると、新しく作成されたスレッドが停止状態に移行し、そのまま停止し続けるため プログラムが終了しない


this.wait() メソッドを呼び出したスレッドは、モニタから一度抜けてウェイトセットへ移動する。そのため、this オブジェクトのモニタには他のスレッドが入れるようになる。

このスレッドを 1 秒後に再開させるには、例えば次のようなプログラムを書けばよい。

public static void main(String[] args) throws InterruptedException {
    // 待機用のオブジェクト
    WaitObject wait = new WaitObject();
    WaitTask task = new WaitTask(wait);
    Thread thread = new Thread(task);
    
    thread.start();
    
    // 一秒待ってから
    Thread.sleep(1000);
    
    // 待機状態を解除
    wait.resume();
}

このプログラムは、先ほどのプログラムと同様に新しいスレッドで WaitTask の run() メソッドを実行するが、その1秒後に WaitObject の resume() メソッドを呼び出して、新しいスレッドの停止状態を解除している。


this.notifyAll() メソッドを呼び出すことによって、ウェイトセット内で停止しているスレッドを起こすことができる。起きたスレッドはモニタに再度入ろうとするが、this.notifyAll を実行したスレッドがモニタに入っているため、一時的に待ち状態になる。


this.notifyAll() メソッドを実行していたスレッドがモニタから抜けると、停止していたスレッドは this.wait() の続きから実行する。

少し難解であるが、この流れは覚えておくとプログラムの幅を広げることができる。ただし、よく考えてこれらのメソッドを実行しないと、wait したまま起きられないスレッドなどが発生してプログラムが正常に動作しなくなる。

synchronized ブロック (optional)

オブジェクトのモニタに入るためには synchronized で修飾されたメソッドを実行すればよい。この場合は、そのメソッドの this オブジェクトのモニタに入る。

どのオブジェクトのモニタに入るかを細かく指定する場合、synchronized ブロック という機構を使用するとよい。

synchronized (mutex) {
    ...
}

これは、if 分や while 文などと同様にメソッド内の任意の場所に書くことができ、上記の mutex で表したオブジェクトのモニタに入る。

まとめ

java.lang.Thread

java.lang.Thread はプログラムの「制御の糸」をカプセル化したクラスである。

次のようなメソッドを紹介した。

java.lang.Runnable

java.lang.Runnable は run() というメソッドを持つインターフェースである。

このインターフェースを実装するクラスを作成し、Thread のコンストラクタ引数にインスタンスを渡すことによって、新しいスレッドで run() メソッドを実行することができる。

Thread.start

run() メソッドを新しいスレッドで実行する。

Thread.sleep

指定した時間だけ、スレッドを一時停止させる。時間はミリ秒で指定する。

クラス (static) メソッドであることに注意。

Thread.join

対象のスレッドが終了するまで待機する。

Thread.interrupt

対象のスレッドに割り込みを掛ける。対象のスレッドが Thread.sleep, Thread.join, Object.wait などの休止状態に入るメソッドを実行している場合や、実行しようとした場合に java.lang.InterruptedException がスローされ、休止状態を打ち切る。

synchronized

メソッドの修飾子。この修飾子が付いたメソッドは、this オブジェクトのモニタに入らないと実行できない。

モニタはオブジェクト一つ一つに用意されていて、同時に一つのスレッドだけが入ることができる。つまり、同時に synchronized メソッドを二つ以上のスレッドで実行できない (オブジェクトが異なれば問題ない)。

モニタに関する用語は「入る」/「抜ける」よりも「獲得する」/「開放する」などのほうが一般的であるかもしれない。また、モニタではなく「ロック」と呼ばれることもある。

Object.wait, Object.notifyAll

wait メソッドを実行したスレッドは、指定したオブジェクトのウェイトセットに入り、プログラムの実行を一時的に停止する。

逆に、notifyAll メソッドを実行すると、そのオブジェクトのウェイトセットに入っているスレッドを全て起こし、プログラムの実行を再開させることができる。

どちらも、そのオブジェクトのモニタに入った状態でないと使用できない。そうしないと java.lang.IllegalMonitorStateException がスローされる。