[索引]
マルチスレッドの起動 |
スレッドとプロセス |
スレッドの操作 |
スレッドのライフサイクル |
スレッドの同期 |
スレッド間通信
スレッド(thread)とは糸のことで、処理の流れを糸に例えたものです。 通常のプログラムは1つの処理だけが走りますが、Javaにはマルチスレッドと呼ばれる機能があって、比較的簡単に複数の処理を同時に走らせることができます。 とはいっても、マルチスレッドの仕組みはやはり複雑です。 ここでは、マルチスレッドの概要を紹介します。
プログラムの起動時にはJavaのスレッドは1つです。
このスレッドをメインスレッドと言います。
新たなスレッドを動作させるには次のようにします。
(1) まずThreadクラスを継承したクラスを定義する。
(2) そのクラスのrunメソッドに別スレッドで動作させたいプログラムを記述する。
class NewThread extends Thread {
public void run() {
// 別スレッドで実行させたいプログラム
}
}
(3) 次に別のスレッドで、(1)で作ったクラスをnewする。
(4) そしてそのインスタンスのstartメソッドを実行する。
NewThread nt = new NewThread();
nt.start();
// 次の処理
すると、(2)で作ったプログラムが実行を開始します。 一方、メインスレッドのプログラムは次の処理へ進みますので2本のスレッドが走っていることになります。
Javaでは多重継承ができないので、すでに継承をしているクラスはThreadクラスを継承することができません。
そのために、次のような手段が提供されています。
(1) Runnableインターフェースを実装したクラスを定義する。
(2) そのクラスのrunメソッドに別スレッドで動作させたいプログラムを記述する。
class MyRunnable implements Runnable {
public void run() {
// 別スレッドで実行させたいプログラム
}
}
(3) 動作中のスレッドで、(1)で作ったクラスをnewする。
(4) それを引き数にしてThreadクラスのインスタンスを生成する。
(5) そのインスタンスのstartメソッドを実行する。
MyRunnable mr = new MyRunnable();
Thread nt = new Thread(mr);
nt.start();
// 次の処理
並行処理を表わす言葉としてマルチプロセスやマルチタスクがありますが、マルチスレッドはこれらとどう違うのでしょうか。 通常は、プロセスやタスクはOSが管理する大がかりな並行処理機構であるのに対し、スレッドはアプリケーション(JavaVMもOSから見ればアプリケーション)が管理する軽量級の並行処理を指します。 つまり1つのプロセスの中に複数のスレッドが生成されるという関係になります。
また、プロセスが固有のメモリ空間を持つのに対し、スレッドでは複数のスレッドがメモリ空間を共有しています(ただしスタックは各スレッドごとに持つ)。 したがって、スレッドはプロセスに比べて軽快ですが、変数の操作には警戒を要すると言うわけです。
Javaではローカル変数(メソッド内で定義された変数)はスタックに格納されます。 上述のようにスタックは各スレッドごとに値を持つので、ローカル変数はスレッドごとの値を持つことができます。 一方、static変数やインスタンス変数は、複数スレッド間で共有されます。
したがって、マルチスレッドで動かすシステムで、後者の変数を扱うときには、他スレッドと競合しないように、必要に応じて他のスレッドからのアクセスを禁止(ロック)しなければなりません(ロックについては後述)。 このようにマルチスレッドでも問題なく動作するプログラムの作りを、スレッドセーフと言います。 サーブレットのようにマルチスレッドで動くプログラムは、スレッドセーフに作らねばなりません。
並行処理といっても、複数のCPUがない限り、本当に同時に処理しているわけではありません。 短い時間で処理を振り分け、同時に動いているように見せているだけです。 マルチスレッドは、同時に動いているように見せる処理を実現するのに威力を発揮します。
スレッドを操作するメソッドをいくつか説明します。
sleepメソッドを使うと、スレッドの処理を一時停止させることができます。 停止時間は引き数でミリ秒単位で指定します。 sleepメソッドはThreadクラスのstaticメソッドなので、クラス名.メソッド名で呼び出します。 例えば3秒間停止させるときは、次のようにします。
Thread.sleep(3000);
sleepメソッドはInterruptedExceptionをスローするので、次のようにtry/catchしなければなりません。 catchブロックの中は何も書かなくてかまいません。
try{
Thread.sleep(3000);
} catch(InterruptedException e) { }
interruptメソッドはsleepメソッドなどで停止中のスレッドに割り込み、停止状態を強制的に解除させることができます。 interruptメソッドは、Threadクラスのインスタンスメソッドなので、次のように、インスタンス名.メソッド名で呼び出します。
Mythread mt = new MyThread();
mt.start();
~
mt.interrupt();
joinは相手のスレッドが終了するまで待つメソッドです。 joinはインスタンスメソッドであり、かつInterruptedExceptionをスローするので、次のように呼び出します。
Mythread mt = new MyThread();
mt.start();
~
try {
mt.join();
} catch(InterruptedException e) { }
// この時点ではmtが終了していることが保証されている
スレッドはnewされると、新規作成状態になり、startメソッドで起動されると実行可能状態になります。 実行可能状態のスレッドを実行状態にするのは、JavaVMのスレッドのスケジューラーの仕事であり、プログラマーは関与することができません。

実行状態のスレッドはsleepや後述のwaitメソッドが実行されたり、入出力動作で待ちが生じたりすると待機状態になります。 待機状態になった原因が取り除かれてもすぐには実行状態にはなりません。 まずは、実行可能状態に移動し、スケジューラーが実行状態にしてくれるのを待つことになります。
実行状態にあるスレッドが、強制的に実行可能状態に移されることがあります。 これもスケジューラーの仕事であり、プログラマーは関与することができません。
実行状態のスレッドの実行が進み終了する(プログラムの最後まで行く)と終了状態になります。 終了状態のスレッドは二度と実行可能状態や実行状態になることはできません。
スレッドには優先度があります。 複数のスレッドが、実行可能状態にあるとき、スケジューラーは優先度の高いスレッドから先に実行状態に割り当てようとします。 同じ優先度のタスクが複数あるときは、スケジューラーがそれらを時分割で割り当てようとします。
※ スレッドの強制的な中断や、高い優先度のスレッドの割り当ては、JavaVMが動作するOSの機能により実現されているので、Java言語の仕様として必ずしも保証されているものではありません。
共有のデータを複数のスレッドが扱うと、データの競合が発生する可能性があります。 それは以下の処理で、あるスレッドが処理を開始し(2)の状態にあるときに、別のスレッドが(1)の処理を開始するような場合です。 これはまさに前述のスレッドセーフでない状況です。
(1)共有域のデータを取り込む
(2)取り込んだデータを加工する
(3)加工したデータを共有域に書き込む
このようなことが起こらないようにするには、あるスレッドが上記の(1)~(3)の処理をしている間は、他のスレッドはこの処理ができないようにロックすれば良いのです。 このように排他的な操作によって複数のスレッド間で矛盾が起こらないようにすることを、同期をかける(synchronize)と言います。
同期をかけるには、次のようにメソッドにsynchronizedキーワードを付けます。 するとこのメソッドを含むインスタンスに同期がかかり、他のスレッドはそのインスタンスのsynchronizedメソッドは実行できなくなります(実行が終わるまで待機状態で待たされます)。 そのインスタンスのsynchronizedでないメソッドは自由に実行できます。 また、同じクラスから生成されたものでも、別インスタンスのメソッドは、実行できます。
synchronized void mathodA() { … // メソッド単位で設定
メソッド全体でなく、メソッドの一部分だけで同期をかけることができます。 それには次のようにsynchronizedブロックを使います。 この場合同期をかけるオブジェクトを指定します。
synchronized (obj) { … // ブロック単位で設定
同期をかけると、他の処理をロックすることになるので、一般に性能の低下をもたらします。 ロックは必要最小限にしましょう。
同期をかけるときには、デッドロックにならないように注意が必要です。 デッドロックとは、2つのスレッドが互いに相手の同期の開放を待っている状態を指します。 この状態になると、どちらのスレッドも処理が進まず、回復は不可能です。 デッドロックは、同期をかける箇所が複数なければ発生しません。 デッドロックを避けるには、同期をかけるオブジェクトの順序を一定にすることがポイントです。
複数のスレッド間で協調して処理を行う場合、あるスレッドの部分処理が終わるのを、他方のスレッドが待ちたい場合があります。 このような場合にwait/notify/notifyAllメソッドを使います。
waitメソッドは、notifyまたはnotifyAllメソッドが呼び出されるまで処理を待機させます。 waitメソッドはObjectクラスのメソッドなので、すべてのクラスからそのまま使えます。 スレッド間通信でwaitメソッドを使うには、synchronizedキーワードで同期をとっておかねばなりません。 それはwaitがこの同期の仕組みを使って待ち合わせをするように作られているからです。 また、waitメソッドはInterruptedExceptionをスローするので、次のようにtry/catchする必要があります。
synchronized(this) {
try{
wait();
} catch(InterruptedException e) { }
}
waitメソッドは、次のように整数値を引き数にとることができます。 こうすると、引き数で示した時間後(単位はミリ秒)に実行可能状態なります。 引き数がないと、次項のnotify/notifyAllが出されるまで、復帰しません。
wait(5000) // 5秒後に実行可能状態になる
notifyメソッドは、waitメソッドによって待機状態になっているスレッドを実行可能状態にします。 待機中のメソッドが複数あるときは、そのうちのどれか1つだけを、実行可能状態にします。 notifyメソッドはObjectクラスのメソッドなのですべてのクラスからそのままアクセスできます。 notifyメソッドを実行するためには、waitメソッドの場合と同じく、該当オブジェクトのロックを取得していなければなりません。 try/catchする必要はありません。
synchronized(this) {
notify();
}
notifyが待機中のメソッドのどれか1つだけを実行可能状態にしたのに対し、notifyAllは待機中のすべてのメソッドを実行可能状態にします。 ただし、実際に実行状態になるのはそのうちの、いずれか1つだけです。 その他の働きはnotifyメソッドと同様です。