<概要>もっとスレッドを学びます。難しさもさらなる高みへ……。
●例題16-2-1 スレッドを任意のタイミングで終了させるには?
○プログラム
class Runner extends Thread{
private boolean running=true;
public void stopRunning(){
running=false;
}
public void run(){
for(int i=0;running;i++){
System.out.println("run i="+i);
}
}
public static void main(String[] args){
Runner runner=new Runner();
runner.start();
for(int i=0;i<1000;i++){
System.out.println("main i="+i);
}
runner.running=false;
}
}
○実行結果
……
run i=1023
run i=1024
run i=1025
main i=993
main i=994
main i=995
main i=996
main i=997
main i=998
main i=999
-- Press any key to exit (Input "c" to continue) --
○解説
スレッドが自分自身を終了させたい場合は、
run メソッドを終わらせれば良いだけのことです。
自分以外のスレッドを終了させたいときは、
終了させたいスレッドにあらかじめ終了条件を埋め込んでおき、
外からその条件を操作すれば良いのです。
終了条件とは run メソッドを終了させるための条件なので、
要するに、自分であれ自分以外であれ、
スレッドを終了させるためには run メソッドを終了させることです。
○補足
Thread クラスには、stop というメソッドがあります。
これはスレッドを強制的に停止するメソッドですが、「推奨しないAPI」として扱われています。
ヘルプにも、スレッドを終了させたいならば、
上に示したプログラムのように条件を操作して run メソッドを終了させるべきだと書いてあります。
○補足2
ふと疑問に思ったのが「run メソッドの局所変数はスレッドが切り替わることでリセットされないのか?」ということ。
しかし、実行結果からも明らかなように全ての値は保持されています。
よく考えてみれば、もう一度 run メソッドを呼び出すわけじゃなく、
途中から再開しているんですね。
run メソッドは main メソッドと同じだと考えれば当然の結論が得られましたとさ。
●例題16-2-2 スレッドを一時停止させるには?
○プログラム
public class Periodic{
public static void main(String[] args){
for(int i=0;i<10;i++){
int tm=i*1000;
System.out.println("Start sleep:tm="+tm);
try{
Thread.sleep(tm);
}catch(InterruptedException e){
System.out.println(e);
}
}
}
}
○実行結果
D:\atsushi\Java\List16-8>java Periodic
Start sleep:tm=0
Start sleep:tm=1000
Start sleep:tm=2000
Start sleep:tm=3000
Start sleep:tm=4000
Start sleep:tm=5000
Start sleep:tm=6000
Start sleep:tm=7000
Start sleep:tm=8000
Start sleep:tm=9000
-- Press any key to exit (Input "c" to continue) --
○解説
スレッドを一時停止させたい場合は、Thread クラスの sleep メソッドを使います。
さて、ここで例外処理の復習ですが、例外を投げるメソッドを呼び出す場合は、
try ブロックでくくり、catch ブロックでキャッチするか、
try{
Thread.sleep(tm);
}catch(InterruptedException e){
System.out.println(e);
}
例外を投げるメソッドを呼び出しているメソッドに throws 節を書かなければなりません。
public static void main(String[] args) throws InterruptedException{
public static void sleep(long millis) throws InterruptedException //Thread クラス ← java.lang パッケージ
現在実行中のスレッドを、指定されたミリ秒数の間、スリープ (一時的に実行を停止) させます。
スレッドはモニターの所有権を失いません。
パラメータ: millis - ミリ秒単位のスリープ時間の長さ
例外: InterruptedException - 別のスレッドが現在のスレッドに割り込んだ場合。
この例外がスローされると、現在のスレッドの割り込みステータスはクリアされる
public static void sleep(long millis,int nanos) throws InterruptedException //Thread クラス ← java.lang パッケージ
●例題16-2-3 任意のスレッドが終了するまで待機させるには?
○プログラム
public class JoinTest extends Thread{
public static void main(String[] args){
JoinTest th=new JoinTest();
System.out.println("main:はじめ");
th.start();
System.out.println("main:終了待ちに入る");
try{
th.join();
}catch(InterruptedException e){
System.out.println(e);
}
System.out.println("main:おわり");
}
public void run(){
System.out.println("run:スレッド実行開始");
try{
Thread.sleep(5000);
}catch(InterruptedException e){
System.out.println(e);
}
System.out.println("run:スレッド実行終了");
}
}
○実行結果
D:\atsushi\Java\List16-10>java JoinTest
main:はじめ
main:終了待ちに入る
run:スレッド実行開始
run:スレッド実行終了
main:おわり
-- Press any key to exit (Input "c" to continue) --
○解説
Thread クラスの join メソッドを使うと、
そのオブジェクトのスレッドが終了するまで他のスレッドは待機することになります。
実行結果で表示されている文字列の順番を見ると、
確かに run メソッドが終了するまで main メソッドは待機していることが分かります。
public final void join() throws InterruptedException //Thread クラス ← java.lang パッケージ
このスレッドが終了するのを待機します。
例外: InterruptedException - 別のスレッドが現在のスレッドに割り込んだ場合。
この例外がスローされると、現在のスレッドの割り込みステータスはクリアされる
public final void join(long millis) throws InterruptedException //Thread クラス ← java.lang パッケージ
public final void join(long millis,int nanos) throws InterruptedException //Thread クラス ← java.lang パッケージ
●例題16-2-4 スレッドの同期を取るには?
○プログラム
非常に長いので別窓でご用意致しました( click here )。
プログラムよりも先に解説をお読み下さい。
それほど難しくはないのですが、とにかく長いプログラムですので。
○実行結果
D:\atsushi\Java\List16-11>java ProducerConsumer
Thread-1 wait:データを待つ
Producer: Thread-0 は 0 を生産完了
Consume: Thread-1 は 0 を消費中
Producer: Thread-0 は 1 を生産完了
Producer: Thread-0 は 2 を生産完了
Producer: Thread-0 は 3 を生産完了
Producer: Thread-0 は 4 を生産完了
Thread-0 wait:バッファの空きを待つ
Consume: Thread-1 は 1 を消費中
Producer: Thread-0 は 5 を生産完了
Thread-0 wait:バッファの空きを待つ
Consume: Thread-1 は 2 を消費中
Producer: Thread-0 は 6 を生産完了
Thread-0 wait:バッファの空きを待つ
Consume: Thread-1 は 3 を消費中
……
○解説
先入れ先出し方式のリングバッファ(上書きはしない)を表現しています。
バッファが一杯ならば、空き領域が出来るまで待たなければなりませんし、
バッファが空ならば、データが格納されるまで待たなければなりません。
待っている間、永久ループで監視し続けるのはマシンパワーの無駄使いです。
したがって、お互いに連絡を取り合って、動作を制御する必要があります。
このような機構を同期と呼びます。
それでは、長大なプログラムですが、少しずつ解析していきましょう。
//Buffer クラス
スレッドの制御を行いながらデータの出し入れを行います。
マルチスレッドの環境でも正しく動作するクラスやメソッドのことをスレッドセーフと呼びます。
ここで宣言した Buffer クラスは、スレッドセーフなクラスです。
12. int[] intbuf;
先入れ先出し方式のリングバッファ(上書きはしない)です。
13. int start;
有効なデータが格納されている先頭の添字を表しています。
次に取り出す配列の添字ということになります。
14. int count;
有効なデータの個数を表しています。
start+count で、次にデータを格納する配列の添字になります。
20. public synchronized void append(int n){
インスタンスフィールドを扱うので、排他制御が必要です。
21. while(count>=intbuf.length){
バッファに一つでも空き領域があるか判定しています。
if ではなく while なのがポイントです。
22. System.out.println(Thread.currentThread().getName()+" wait:バッファの空きを待つ");
public final String getName() //Thread クラス ← java.lang パッケージ
このスレッドの名前を返します。
戻り値: このスレッドの名前
24. wait();
一つも空き領域が無ければ、空き領域が出来るまで待ちます。
wait は Object クラスのメソッドです。
このことは、全てのオブジェクトはロックの対象になるということを意味しています。
wait メソッドを実行したスレッドはオブジェクト毎に用意されたウエイトセットという待合室に入ります。
そして、notify されるまで実行を一時停止することになります。
public final void wait() throws InterruptedException //Object クラス ← java.lang パッケージ
他のスレッドがこのオブジェクトの notify() メソッド
または notifyAll() メソッドを呼び出すまで、現在のスレッドを待機させます。
例外: IllegalMonitorStateException - 現在のスレッドがオブジェクトのモニターを所有していない場合
InterruptedException - 別のスレッドが現在のスレッドに割り込んだ場合。
この例外がスローされると、現在のスレッドの割り込みステータスはクリアされる
29. int end=(start+count)%intbuf.length;
次にデータを格納する添字を求めています。
リングバッファなので添字はぐるぐる回ります。
count>=intbuf.length が成り立たないということは、
この時点で end の領域は空いていることが保証されます。
したがって、上書きされることはありません。
31. count++;
有効なデータの個数を変更しています。
notifyAll メソッドを呼び出すのはこれ以降でなければなりません。
32. notifyAll();
データが格納されたので、データ無しで待っているかも知れない他のスレッドの処理を再開させます。
正確には、notifyAll メソッドを発行したスレッドが、
そのオブジェクトのロックを解放しない限り、他のスレッドは走り出しません。
今の場合だと「synchronized メソッド append を抜けたとき」ということになります。
もし仮に、append を呼び出したのが synchronized メソッドであれば
「呼び出した synchronized メソッドを抜けたとき」です。
notifuAll が発行されても、wait する条件をまだ満たしているかも知れないので、
もう一度判定しなければなりません。
その為に、if ではなく while が使われていたのですね。
public final void notifyAll() //Object クラス ← java.lang パッケージ
このオブジェクトのモニターで待機中のすべてのスレッドを再開します。
スレッドは、wait メソッドを呼び出すと、オブジェクトのモニターで待機します。
例外: IllegalMonitorStateException - 現在のスレッドがこのオブジェクトのモニターを所有していない場合
public final void notify() //Object クラス ← java.lang パッケージ
このオブジェクトのモニターで待機中のスレッドを 1 つ再開します。
このオブジェクトで複数のスレッドが待機中の場合は、そのうちの 1 つを再開します。
この選択は任意で、実装によって異なります。
スレッドは、wait メソッドを 1 つ呼び出して、オブジェクトのモニターで待機します。
例外: IllegalMonitorStateException - 現在のスレッドがこのオブジェクトのモニターを所有していない場合
wait や notify や notifyAll を発行するメソッドが、
必ずしも synchronized メソッドである必要はありません。
しかし、wait や notify や notifyAll を実行するとき、対象となるオブジェクトにロックを掛けなければなりません。
ロックを掛ける代表的な方法が synchronized メソッドなのです。
もともとロックを掛けていなければ、複数のスレッドがそのオブジェクトに干渉しつつ動くことになるので、
wait や notify や notifyAll が期待した動作をすることは出来なくなるでしょう。
もしも、そのオブジェクトにロックを掛けていないスレッドが wait や notify
や notifyAll を発行しようとすると
IllegalMonitorStateException という例外が発生します。
次は個人的に納得できませんが……
wait は、そのスレッドが持っているオブジェクトのロックをいったん外します。
wait を発行すると、他のスレッドで synchronized ブロックの前で待っていたスレッドが、
動作可能状態になります。
ちなみに、Thread.sleep メソッドはオブジェクトのロックを外しません。
……だそうです(汗)。
よくわからないので、抜粋させてもらいました(笑)。
ロックしなければ wait メソッドを呼び出せないのに、
wait メソッドがロックを外しちゃうなんて矛盾してると思いませんか?
ロックが外れたとしても、wait する条件を満たしていることに変わりはないので、
他のスレッドも wait するのがオチだと思うのですが……。
そもそも、synchronized のロックを外すなんて「排他制御を行う」という目的に反していませんか?
34. public synchronized int remove(){
インスタンスフィールドを扱うので、排他制御が必要です。
35. while(count==0){
データが一つもないのならば、データが格納されるまで待たなければなりません。
43. int n=intbuf[start];
start は、次に取り出す配列の添字を表しています。
count==0 が成り立たないということは、この時点で start の領域にデータがあることが保証されます。
44. start=(start+1)%intbuf.length;
リングバッファなので添字はぐるぐる回ります。
45. count--;
有効なデータの個数を変更しています。
notifyAll メソッドを呼び出すのはこれ以降でなければなりません。
46. notifyAll();
データが取り出されたので、空き領域無しで待っているかも知れない他のスレッドの処理を再開します。
//Producer クラス
データを作成、格納します。
56. public void run(){
57. for(int i=0;i<1000;i++){
58. int n=produce(i);
59. buffer.append(i);
60. }
61. buffer.append(-1);
62. }
produce メソッドでデータを作成して、
append メソッドでデータを格納します。
-1 は終了コードです。
70. int n=(int)(Math.random()*10000);
public static double random() //Math クラス ← java.lang パッケージ
0.0 以上で、1.0 より小さい正の符号の付いた double 値を返します。
戻り値は、この範囲からの一様分布によって擬似乱数的に選択されます。
戻り値: double の擬似乱数。範囲は、0.0 以上 1.0 未満
//Consumer クラス
データを取り出します。
83. public void run(){
84. while(true){
85. int n=buffer.remove();
86. if(n==-1){
87. break;
88. }
89. consume(n);
90. }
91. }
remove メソッドでデータを取り出します。
取り出したデータが終了コード -1 ならば、
もう有効なデータはないので、run メソッドを抜けてこのスレッドを終了させます。
プログラムは理解できましたか?
次に実行結果を見ていきましょう。
かなり分かりづらい出力内容となっていますが、
改良しようにもいつスレッドが切り替わるか分からないので、
意図した通りに出力されないんですよー(泣)。
いま、バッファの数は 3 個です。
Thread-1 wait:データを待つ
データを取り出そうとしたが無かった。
スレッド1はウエイトセットに入ります。
Producer: Thread-0 は 0 を生産完了
データは生産完了してもまだ格納はしていません。
スレッド1をウエイトセットから出します。
Consume: Thread-1 は 0 を消費中
スレッド1はウエイトセットから出たので、処理を再開しました。
実は既にデータを取り出しています。
だからこそ、その値が分かるんですが。
Producer: Thread-0 は 1 を生産完了
Producer: Thread-0 は 2 を生産完了
Producer: Thread-0 は 3 を生産完了
Producer: Thread-0 は 4 を生産完了
この時点で、有効なデータは四つになりました。
四つめはまだ格納されていません。
Thread-0 wait:バッファの空きを待つ
データを格納しようとしたが一つも空き領域が無かった。
スレッド0はウエイトセットに入ります。
Consume: Thread-1 は 1 を消費中
データを取り出しました。
スレッド0をウエイトセットから出します。
Producer: Thread-0 は 5 を生産完了
スレッド0はウエイトセットから出たので、処理を再開しました。
この場合は、データを格納しました。
そして、データを生産しました。
……とまあ、こんな感じです。
わ、分かりましたか(汗)?
簡単にまとめると「wait と notify または notifyAll を使って同期を取っている」ということだけは覚えておきましょう。
それにしてもこのプログラム長過ぎですよね……お疲れさまです。
私も疲れました(笑)。
★プライオリティ(優先順位)
スレッドにはプライオリティ(優先順位)があります。
プライオリティが高いスレッドの方が、低いスレッドよりも優先的に実行されます。
public final int getPriority() //Thread クラス ← java.lang パッケージ
このスレッドの優先順位を返します。
戻り値: このスレッドの優先順位
public final void setPriority(int newPriority) //Thread クラス ← java.lang パッケージ
このスレッドの優先順位を変更します。
パラメータ: newPriority - このスレッドを設定する優先順位
例外: IllegalArgumentException - 優先順位が MIN_PRIORITY 〜 MAX_PRIORITY
の範囲外である場合
SecurityException - 現在のスレッドがこのスレッドを変更できない場合
public static final int MIN_PRIORITY //Thread クラス ← java.lang パッケージ
スレッドに設定できる最低優先順位です。
public static final int NORM_PRIORITY //Thread クラス ← java.lang パッケージ
スレッドに割り当てられるデフォルトの優先順位です。
public static final int MAX_PRIORITY //Thread クラス ← java.lang パッケージ
スレッドに設定できる最高優先順位です。