スレッド

<概要>たぶん最難関のマルチスレッドを学びます!

スレッド( thread )とは、プログラムを実行している主体のことを意味します。
今までに作ってきたプログラムは全て、ある時点で行っている作業は一つしかない
シングルスレッドのプログラムでした。
これに対してマルチスレッドは、ある時点で行っている作業が複数あるプログラムです。

ところで、CPUは一つしかないのに、同時に複数の作業をするなんて可能なのでしょうか?
厳密な意味では、複数のスレッドが同時に動いているわけではありません。
しかし、Java仮想マシンというソフトウェアが複数のスレッドを管理してくれるので、
擬似的にマルチスレッドが実現できるのです。

マルチスレッドの利点は何でしょうか?
それはもう、非常に時間のかかる作業をしていても
他の作業を同時にすることが出来る!でしょう。
例えば、ビデオのエンコードをしながらメールを読む、
なんてのはマルチスレッドなればこそです。
たとえ合計の処理時間は変わらなかったとしても、
個々の要求にはマルチスレッドの方が速く応えられるのです。

スレッドを作るには、次の二つの方法があります。

(A) Thread クラスの拡張クラスを作る
(B) Runnable インタフェースを実装したクラスを作る

例題16-1 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) --

○解説

CountTenA ct=new CountTenA();

スレッドを作成します。

ct.start();

スレッドの実行を開始します。
Java 仮想マシンは、このスレッドの run メソッドを呼び出します。
その結果、main メソッドと run メソッドが並列に実行されることになります。

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()


いつスレッドが切り替わるかは環境に依ります。
実はこれが大問題なのですが。

○補足

クラス型変数が不要ならば

CountTenA ct=new CountTenA();
ct.start();


は、

new CountTestA().start();

と書くことも出来ます。

例題16-2 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) --

○解説

Thread th=new Thread(ct);

Runnable インタフェースを実装したクラスを引数にしてスレッドが作られた場合は、
実装クラスの run メソッドが start メソッドより呼び出されます。

public Thread(Runnable target)

新しいスレッドを作る二つの方法の違いは何でしょうか?

(A) Thread クラスの拡張クラスを作る

とにかく手軽なことです。
しかし、スーパークラスは一つしか持てないという欠点があります。

(B) Runnable インタフェースを実装したクラスを作る

継承の欠点を克服するためにあります。
Runnable の実装クラスには run というメソッドが必ず存在することが保証されています。
だから、Thread のコンストラクタは Runnable の実装クラスを引数に取ったんですね。
なるほど、インタフェースを上手く使っていらっしゃる。

○補足

Thread クラスを継承した場合も、Runnable インタフェースを実装した場合も
start メソッドを介さず、run メソッドを直接呼び出すことが出来ます。
それでは、start メソッドを呼び出したときと、run メソッドを呼び出したときの動作の違いは何でしょう?
start メソッドを呼び出した場合、スレッドの実行を開始すると制御は直ぐに戻ってきます。
しかし、直接 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

private int value=0;

インスタンスフィールドです。
private なので、他のクラスからは参照できません。
初期値として 0 を代入しています。
インスタンスフィールドは、インスタンス毎に作られ
インスタンスを通してしかアクセスできないフィールドでしたね。

public void addMoney(int money){

インスタンスメソッドです。
インスタンスメソッドは、インスタンスを通してしかアクセスできないメソッドでしたね。

System.out.println(Thread.currentThread()+" が addMoney に入りました。");

public static Thread currentThread()  //Thread クラス
 現在実行中のスレッドオブジェクトの参照を返します。
戻り値: 現在実行中のスレッド

public String toString()  //Thread クラス
 スレッドの名前、優先順位、スレッドグループを含むこのスレッドの文字列表現を返します。
オーバーライド: クラス Object 内の toString
戻り値: このスレッドの文字列表現

System.exit(-1);

public static void exit(int status)
 現在実行している Java 仮想マシンを終了します。
 引数はステータスコードとして作用します。
 通例、ゼロ以外のステータスコードは異常終了を示します。

//BadBankTest.java

public BadBankTest(BadBank bank){
  this.bank=bank;
}


何故このような処理をしているのかといえば、
それぞれのインスタンスのインスタンスフィールド、インスタンスメソッドにアクセスするためです。

BadBank bank=new BadBank();
new BadBankTest(bank).start();
new BadBankTest(bank).start();


同じインスタンスを渡して二つのスレッドを作成、実行開始しています。

さて、もう問題点にお気付きでしょうか?

二つのスレッドは同じインスタンスを扱っています。
つまり、二つのスレッドは同じインスタンスフィールド value の値を上下させるわけです。
それに対して、局所変数 currentValue は、メソッド呼び出し毎に作成されます。
つまり、二つのスレッドは違う局所変数を使用するわけです。
仮引数 money も局所変数ですね。
この点に注意して以下の解説をお読み下さい。

マルチスレッドは、いつスレッドが切り替わるか分かりません。
例えば、以下のような場合を考えてみましょう。
money には 100 が入ってきたとします。

まず、スレッド1で次のところまで実行されたとします。

public void addMoney(int money){
  int currentValue=value;
  System.out.println(Thread.currentThread()+" が addMoney に入りました。");


スレッド1:value=0 / currentValue=0 / money=100
次に、スレッド2でも同じところまで実行されたとします。
スレッド2:value=0 / currentValue=0 / money=100

次に、スレッド1で次のところまで実行されたとします。

  value+=money;
  if(currentValue+money!=value){
    System.out.println(Thread.currentThread()+" で矛盾が発生しました!");
    System.exit(-1);
  }
  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(this){
synchronized ブロックは、メソッドの一部分だけでロックを掛けることが出来ます。

○補足

synchronized インスタンスメソッドはインスタンスをロックします。
synchronized クラスメソッドはクラスをロックします。
つまり、インスタンスが違っても synchronized クラスメソッドを実行できるのは一つのスレッドだけです。


戻る / ホーム