<概要>最難関のマルチスレッドについて学びます!
スレッド( thread )とは「プログラムを実行している主体」のことを意味します。
今までに作ってきたプログラムは全て「ある時点で行っている作業は一つしかない」
シングルスレッドのプログラムでした。
これに対してマルチスレッドは「ある時点で行っている作業が複数ある」プログラムです。
ところで、CPUは一つしかないのに、同時に複数の作業をするなんて可能なのでしょうか?
厳密な意味では、複数のスレッドが同時に動いているわけではありません。
しかし、Java仮想マシンというソフトウェアが複数のスレッドを管理してくれるので、
擬似的にマルチスレッドが実現できるのです。
マルチスレッドの利点は何でしょうか?
それはもう、非常に時間のかかる作業をしていても
他の作業を同時にすることが出来る!でしょう。
例えば、ビデオのエンコードをしながらメールを読む、
なんてのはマルチスレッドなればこそです。
たとえ合計の処理時間は変わらなかったとしても、
個々の要求にはマルチスレッドの方が速く応えられるのです。
スレッドを作るには、次の二つの方法があります。
(A) Thread クラスの拡張クラスを作る。
(B) Runnable インタフェースを実装したクラスを作る。
●例題16-1 (A) Thread クラスの拡張クラスを作る。
○プログラム
public class CountTenA extends Thread{
public static void main(String[] args){
CountTenA ct=new CountTenA();
ct.start();
for(int i=0;i<10;i++){
System.out.println("main:i="+i);
}
}
public void run(){
for(int i=0;i<10;i++){
System.out.println("run:i="+i);
}
}
}
○実行結果
D:\atsushi\Java\List16-1>java CountTenA
main:i=0
main:i=1
main:i=2
main:i=3
main:i=4
run:i=0
run:i=1
run:i=2
run:i=3
run:i=4
run:i=5
run:i=6
run:i=7
run:i=8
run:i=9
main:i=5
main:i=6
main:i=7
main:i=8
main:i=9
-- Press any key to exit (Input "c" to continue) --
○解説
3. CountTenA ct=new CountTenA();
スレッドを作成します。
4. ct.start();
スレッドの実行を開始します。
Java 仮想マシンは、このスレッドの run メソッドを呼び出します。
その結果、main メソッドと run メソッドが並列に実行されることになります。
9. public void run(){
run メソッドは、スレッドにとっての main メソッドに相当するものです。
Thread のサブクラスは、このメソッドをオーバーライドしなければなりません。
run メソッドの一般的な規約によれば、run メソッドはどのようなアクションを実行しても構いません。
run はインタフェース Runnable で宣言されている抽象メソッドです。
Thread クラスは Runnable インタフェースを実装しています。
public class Thread extends Object implements Runnable
public void start()
public void run()
public interface Runnable
public void run()
実行結果では main:i=4 と run:i=0 の間、
run:i=9 と main:i=5 の間でスレッドの切り替えが起こっていますが、
いつスレッドが切り替わるかは環境に依ります。
実はこれが大問題なのですが、詳しくは例題16-3で解説します。。
○補足
クラス型変数が不要ならば
CountTenA ct=new CountTenA();
ct.start();
は、
new CountTestA().start();
と書くことも出来ます。
●例題16-2 (B) Runnable インタフェースを実装したクラスを作る。
○プログラム
public class CountTenB implements Runnable{
public static void main(String[] args){
CountTenB ct=new CountTenB();
Thread th=new Thread(ct);
th.start();
for(int i=0;i<10;i++){
System.out.println("main:i="+i);
}
}
public void run(){
for(int i=0;i<10;i++){
System.out.println("run:i="+i);
}
}
}
○実行結果
D:\atsushi\Java\List16-2>java CountTenB
main:i=0
main:i=1
main:i=2
main:i=3
main:i=4
main:i=5
main:i=6
run:i=0
run:i=1
run:i=2
run:i=3
run:i=4
run:i=5
run:i=6
run:i=7
run:i=8
run:i=9
main:i=7
main:i=8
main:i=9
-- Press any key to exit (Input "c" to continue) --
○解説
4. Thread th=new Thread(ct);
Runnable インタフェースを実装したクラスを引数にしてスレッドが作られた場合は、
実装クラスの run メソッドが start メソッドにより呼び出されます。
public Thread(Runnable target)
新しいスレッドを作る二つの方法の違いは何でしょうか?
(A) Thread クラスの拡張クラスを作る。
とにかく手軽なことです。
しかし、スーパークラスは一つしか持てないという欠点があります。
(B) Runnable インタフェースを実装したクラスを作る。
継承の欠点を克服する為にあります。
Runnable インタフェースの実装クラスには run というメソッドが必ず存在することが保証されています。
だから、Thread クラスのコンストラクタは Runnable インタフェースの実装クラスを引数に取ったんですね。
なるほど、インタフェースを上手く使っていらっしゃる。
ところで、もうお気付きかと思いますが、
インタフェースを実装したクラスのインスタンスを指すポインタを
インタフェースの変数に代入することも可能です。
このような代入互換性(スーパークラスの変数に代入することも含めて)を基本型への回帰、
そして、サブクラス(実装クラス)を指すポインタを代入された
スーパークラスの変数をサブクラスへキャストすることを派生型への回帰と
「決定版はじめてのC++/塚越一雄」では呼んでいました。
○補足
Thread クラスを継承した場合も、Runnable インタフェースを実装した場合も
start メソッドを介さず、run メソッドを直接呼び出すことが出来ます。
それでは、start メソッドを呼び出したときと、run メソッドを直接呼び出したときの動作の違いは何でしょう?
start メソッドを呼び出した場合、スレッドの実行を開始すると制御は直ぐに戻ってきます。
しかし、直接 run メソッドを呼び出した場合、run メソッドが終了しない限り制御は戻ってきません。
run メソッドを直接呼び出しても、シングルスレッドのままなのです。
★ Thread クラスと Runnable インタフェースのメソッド
public class Thread extends Object implements Runnable //java.lang パッケージ
スレッドとは、プログラム内での実行のスレッドのことです。
Java 仮想マシンでは、アプリケーションは並列に実行される複数のスレッドを使用することができます。
public Thread() //Thread クラス ← java.lang パッケージ
新しい Thread オブジェクトを割り当てます。
自動的に作成される名前は、n を整数とすると "Thread-"+n の形式をとります。
public Thread(Runnable target) //Thread クラス ← java.lang パッケージ
新しい Thread オブジェクトを割り当てます。
自動的に作成される名前は、n を整数とすると "Thread-"+n の形式をとります。
パラメータ: target - その run メソッドが呼び出されるオブジェクト
public void start() //Thread クラス ← java.lang パッケージ
このスレッドの実行を開始します。
Java 仮想マシンは、このスレッドの run メソッドを呼び出します。
その結果、(start メソッドへの呼び出しから復帰する) 現在のスレッドと
(その run メソッドを実行する) 別のスレッドという 2 つのスレッドが並列に実行されます。
例外: IllegalThreadStateException - スレッドがすでに起動していた場合
public void run() //Thread クラス ← java.lang パッケージ
このスレッドが別個の Runnable 実行オブジェクトを使用して作成された場合、
その Runnable オブジェクトの run メソッドが呼び出されます。
そうでない場合、このメソッドは何も行わずに復帰します。
Thread のサブクラスは、このメソッドをオーバーライドしなければなりません。
定義: インタフェース Runnable 内の run
public interface Runnable //java.lang パッケージ
インスタンスを 1 つのスレッドで実行するすべてのクラスでは、
Runnable インタフェースを実装する必要があります。
このクラスは、引数のないメソッド run を定義しなければなりません。
public void run() //Runnable インタフェース ← java.lang パッケージ
オブジェクトが実装するインタフェース Runnable を使ってスレッドを作成し、
そのスレッドを開始すると、独立して実行されるスレッド内で、オブジェクトの
run メソッドが呼び出されます。
run メソッドの一般的な規約によれば、run メソッドはどのようなアクションを実行してもかまいません。
●例題16-3 次のプログラムの問題点を指摘して下さい。
○プログラム
//BadBank.java
public class BadBank{
private int value=0;
public void addMoney(int money){
int currentValue=value;
System.out.println(Thread.currentThread()+" が addMoney に入りました。");
value+=money;
if(currentValue+money!=value){
System.out.println(Thread.currentThread()+" で矛盾が発生しました!");
System.exit(-1);
}
System.out.println(Thread.currentThread()+" が addMoney から出ました。");
}
}
//BadBankTest.java
public class BadBankTest extends Thread{
BadBank bank;
public BadBankTest(BadBank bank){
this.bank=bank;
}
public void run(){
while(true){
bank.addMoney(100);
bank.addMoney(-100);
}
}
public static void main(String[] args){
BadBank bank=new BadBank();
new BadBankTest(bank).start();
new BadBankTest(bank).start();
}
}
○実行結果
……
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-1,5,main] で矛盾が発生しました!
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-0,5,main] が addMoney に入りました。
-- Press any key to exit (Input "c" to continue) --
○解説
これが分かったあなたは凄い!
問題点は BadBank.java の方にあります。
ヒントは二つのスレッドで同じインスタンスを扱っていることです。
それでは、プログラムを見ていきましょう。
問題点は個々のプログラムを解説してから明らかにします。
//BadBank.java
2. private int value=0;
インスタンスフィールドです。
private なので、他のクラスからは参照できません。
初期値として 0 を代入しています。
インスタンスフィールドは、インスタンス毎に作られ、
インスタンスを通してしかアクセスできないフィールドでしたね。
3. public void addMoney(int money){
インスタンスメソッドです。
インスタンスメソッドは、インスタンスを通してしかアクセスできないメソッドでしたね。
5. System.out.println(Thread.currentThread()+" が addMoney に入りました。");
public static Thread currentThread() //Thread クラス ← java.lang パッケージ
現在実行中のスレッドオブジェクトの参照を返します。
戻り値: 現在実行中のスレッド
public String toString() //Thread クラス ← java.lang パッケージ
スレッドの名前、優先順位、スレッドグループを含むこのスレッドの文字列表現を返します。
オーバーライド: クラス Object 内の toString
戻り値: このスレッドの文字列表現
9. System.exit(-1);
public static void exit(int status) //System クラス ← java.lang パッケージ
現在実行している Java 仮想マシンを終了します。
引数はステータスコードとして作用します。
通例、ゼロ以外のステータスコードは異常終了を示します。
パラメータ: status - 終了のステータス
例外: SecurityException - セキュリティマネージャが存在し、その checkExit
メソッドが、
指定されたステータスでの終了を許可しない場合
//BadBankTest.java
3. public BadBankTest(BadBank bank){
4. this.bank=bank;
5 .}
何故このような処理をしているのかといえば、
それぞれのインスタンスのインスタンスフィールド、インスタンスメソッドにアクセスする為です。
インスタンスフィールド、インスタンスメソッドはインスタンスを通してしかアクセスできないんでしたね。
13. BadBank bank=new BadBank();
14. new BadBankTest(bank).start();
15. new BadBankTest(bank).start();
同じインスタンスを渡して二つのスレッドを作成、実行開始しています。
さて、もう問題点にお気付きでしょうか?
……気付いていたら凄いですけれど(笑)。
二つのスレッドは同じインスタンスを扱っています。
つまり、二つのスレッドは同じインスタンスフィールド value の値を上下させるわけです。
それに対して、局所変数 currentValue は、メソッド呼び出し毎に作成されます。
つまり、二つのスレッドは違う局所変数を使うわけです。
仮引数 money も局所変数ですね。
この点に注意して以下の解説をお読み下さい。
マルチスレッドは、いつスレッドが切り替わるか分かりません。
例えば、以下のような場合を考えてみましょう。
仮引数 money に 100 が入ってきたとします。
まず、スレッド1で次のところまで実行されたとします。
3. public void addMoney(int money){
4. int currentValue=value;
5. System.out.println(Thread.currentThread()+" が addMoney に入りました。");
スレッド1:value=0 / currentValue=0 / money=100
次に、スレッド2でも同じところまで実行されたとします。
スレッド2:value=0 / currentValue=0 / money=100
次に、スレッド1で次のところまで実行されたとします。
6. value+=money;
7. if(currentValue+money!=value){
8. System.out.println(Thread.currentThread()+" で矛盾が発生しました!");
9. System.exit(-1);
10. }
11. System.out.println(Thread.currentThread()+" が addMoney から出ました。");
スレッド1:value=100 / currentValue=0 / money=100
if 文の条件式は false になり、矛盾は発生しませんでした。
次に、スレッド2でも同じところまで実行されたとします。
ここで問題が発生します。
value は二つのスレッドで共有されているので、その値は既に 100 になっています。
そこに 100 を加えるのですから value の値は 200 になります。
スレッド2:value=200 / currentValue=0 / money=100
if 文の条件式は true になり、矛盾が発生したことを知らせてくれます。
シングルスレッドであれば何も問題なかったプログラムが
マルチスレッドでは問題となってしまうこの恐ろしさ!
設計からして考え直す必要があるみたいですね。
System.exit(-1); はプログラムそのものを終了させるメソッドですが、
もう一つのスレッドが動き続けていられるのは、
終了処理が完了する前にスレッドが切り替わった、などの理由が考えられます。
●例題16-4 例題16-3のプログラムを訂正する。
○プログラム
//GoodBank.java
public class GoodBank{
private int value=0;
public synchronized void addMoney(int money){
int currentValue=value;
System.out.println(Thread.currentThread()+" が addMoney に入りました。");
value+=money;
if(currentValue+money!=value){
System.out.println(Thread.currentThread()+" で矛盾が発生しました!");
System.exit(-1);
}
System.out.println(Thread.currentThread()+" が addMoney から出ました。");
}
}
//GoodBankTest.java
public class GoodBankTest extends Thread{
GoodBank bank;
public GoodBankTest(GoodBank bank){
this.bank=bank;
}
public void run(){
while(true){
bank.addMoney(100);
bank.addMoney(-100);
}
}
public static void main(String[] args){
GoodBank bank=new GoodBank();
new GoodBankTest(bank).start();
new GoodBankTest(bank).start();
}
}
○実行結果
……
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-1,5,main] が addMoney に入りました。
Thread[Thread-1,5,main] が addMoney から出ました。
……
○解説
synchronized というキーワードをメソッドの返却値型の直前に付けただけです。
→ public synchronized void addMoney(int money){
同じインスタンスの同じ synchronized メソッドは一つのスレッドしか実行できません。
他のスレッドは実行直前で待たされるのです。
このような機構を一般に排他制御を呼びます。
注意して欲しいのは、インスタンスが違えば synchronized メソッドでも同時実行できる、ということです。
スレッドは synchronized メソッドに入るとロックを掛け、
synchronized メソッドから出るとロックを外します。
このようにして矛盾が発生するのを防いでいるのですね。
とりあえず問題は解決しましたが、
マルチスレッドの利点が失われる気がしないでもないと感じるのは私だけ……?
●例題16-5 例題16-4の別解。
○プログラム
//GoodBank.java
public class GoodBank{
private int value=0;
public void addMoney(int money){
synchronized(this){
int currentValue=value;
System.out.println(Thread.currentThread()+" が addMoney
に入りました。");
value+=money;
if(currentValue+money!=value){
System.out.println(Thread.currentThread()+" で矛盾が発生しました!");
System.exit(-1);
}
System.out.println(Thread.currentThread()+" が addMoney
から出ました。");
}
}
}
//GoodBankTest.java
public class GoodBankTest extends Thread{
GoodBank bank;
public GoodBankTest(GoodBank bank){
this.bank=bank;
}
public void run(){
while(true){
bank.addMoney(100);
bank.addMoney(-100);
}
}
public static void main(String[] args){
GoodBank bank=new GoodBank();
new GoodBankTest(bank).start();
new GoodBankTest(bank).start();
}
}
○実行結果
……
Thread[Thread-0,5,main] が addMoney に入りました。
Thread[Thread-0,5,main] が addMoney から出ました。
Thread[Thread-1,5,main] が addMoney に入りました。
Thread[Thread-1,5,main] が addMoney から出ました。
……
○解説
synchronized メソッドの代わりに synchronized ブロックを使いました。
synchronized を付ける場所が変わっていますね。
→ synchronized(this){
synchronized ブロックは、メソッドの一部分だけにロックを掛けることが出来ます。
○補足
synchronized インスタンスメソッドはインスタンスをロックします。
synchronized クラスメソッドはクラスをロックします。
つまり、インスタンスが違っても synchronized クラスメソッドを実行できるのは一つのスレッドだけです。