JAVA programing
マルチスレッド
Javaの重要な特徴として
マルチスレッド(multi threads)
に対応していることが挙げられます。他の言語にはないというわけではありませんが、Javaでは標準で対応しています。Javaでアプリケーションを作る上でマルチスレッドは避けて通れないのでがんばりましょう。
その前にマルチスレッドって何だ。
まずは以下のプログラムを実行してみてください。
public class CountTest{ public static void main(String args[]){ Count c1 = new Count(); c1.print(); for(int i=0; i<20; i++) { System.out.println("main : " + i); } } } class Count{ public void print(){ for(int i=0; i<20; i++) { System.out.println("count : " + i); } } }
どうだったでしょうか。予想通りでしたか?
このプログラムの流れは以下のようになります。
CountTest.main()
↓
Countクラスのオブジェクトを生成
↓
c1.print()文の実行
↓
Countクラスprint()メソッド中のfor文の実行
↓
mainメソッド中のfor文の実行
ここでみてきた制御の流れを
スレッド
と呼びます。先の例ではスレッドは1つしかありませんでしたので、シングルスレッドなプログラムといいます。
では、次に、以下のプログラムを実行してみてください。
public class CountTest2{ public static void main(String args[]){ Count c1 = new Count(); c1.start(); for(int i=0; i<20; i++) { System.out.println("main : " + i); } } } class Count extends Thread{ public void run(){ for(int i=0; i<20; i++) { System.out.println("count : " + i); } } }
どうだったでしょうか。先とは異なる結果になったと思います。
プログラム自体はほとんどいっしょですが、どうしたことでしょう。
実は、このプログラムには制御の流れが2つ、つまりスレッドが2つあるのです。 このプログラムの流れは以下のようになります。
CountTest2.main()
↓
Countクラスのオブジェクトを生成
↓
c1.start()
→
run()
↓
↓
mainメソッド中のfor文の実行
Countクラスprint()メソッド中のfor文の実行
c1.start()という文により新たにスレッドが生成されます。新たに生成されたスレッドはAPIの仕様(Threadクラスの仕様)によりc1オブジェクトのrun()メソッドを実行します。
一方、スレッドを生成した側のスレッド(もともとmain()メソッドから実行を進めてきたスレッド)はc1.start()の次の文に処理を進めます。
2つのスレッドが独立して動作することにより、2つのfor文が同時に動作し、各for文の出力が混ざって表示されたのです。
このように、独立して動作する制御の流れが複数存在するようなプログラムをマルチスレッドなプログラムと呼びます。
マルチプロセス/マルチタスクとマルチスレッドの違い
Windowsは複数のプログラムを同時に動作させることができます。たとえば、InternetExplorerを起動しながらOutlookExpressを起動するといったことができます。2つのプログラムは独立して動作していますが、複数のプロがラムが独立して動作する場合には、一般には
マルチタスク
と呼ばれます。
では、マルチスレッドとマルチタスクと違いは何でしょうか?それはメモリ空間が共有されるかどうかです。
マルチタスク(プロセス)環境では、プロセス間ではメモリ空間は分離されています。そのためデータをプロセス間でやり取りする場合には共有メモリやプロセス間通信のような特殊な枠組みを使う必要があります。
裏を返せば、メモリ空間を分けることで、あるプロセスに異常事態が起きた場合でもほかのプロセスに影響が出ないようにできます。
一方、マルチスレッドではメモリ空間が共有されます。そのため、スレッド間で簡単にデータを共有することができます。共有メモリやプロセス間通信といった特別な機構を使わなくても、通常のプログラミング方法でデータを共有することができるのです。
以下のプログラムを実行してみてください。
public class ShareTest{ public static void main(String args[]){ StringBuffer shareString = new StringBuffer(); ShareSub s1 = new ShareSub( shareString ); s1.start(); for(int i=0; i<20; i++) { System.out.println("shareString = " + shareString); } } } class ShareSub extends Thread{ StringBuffer buf; public ShareSub( StringBuffer buf ){ this.buf = buf; } public void run(){ System.out.println("Sub start "); buf.append("Sub End"); } }
実行結果が途中から変わったのが見てとれると思います。
では、プログラムの中身を見ていきましょう。
はじめ、main()メソッド中で生成したStringBufferクラスのオブジェクトをShareSubクラスのコンストラクタに引数として渡しています。このコンストラクタでは、渡されたStringBufferオブジェクトをbufメンバ変数に格納します。この時点で、main()メソッドのshareString変数と、s1オブジェクトのbuf変数は同一のStringBufferオブジェクトを参照することになり、オブジェクトが共有されることになります。
StringBufferの代わりにStringを使うとこのプログラムは動作しません。Javaにおいては、Stringオブジェクトの値は不変ですので、Stringオブジェクトのデータを変更することはできないからです。なお、代入文で値を変更した場合は、別のStringuオブジェクトを代入していることになり、このオブジェクトは共有しているオブジェクトとは別物になります。
次に、s1.start()でスレッドを生成します。スレッドを生成したもとのスレッドはs1.start()の次のfor文に制御を移します。このfor文ではshareString変数の内容などを画面に出力します。shareString変数の初期値は空文字列なので、何も表示されません。一方、s1.start()で生成されたスレッドは、APIの仕様に従いShareSubクラスのrun()メソッドを呼び出します。このrun()メソッドでは、"Sub Start"という文字列を出力した後、共有しているStringBufferオブジェクトを変更し、"Sub End"という文字列を格納します。このStringBufferオブジェクトは共有されているので、main()側のshareString変数の内容も変更されます。shareString変数の値が変更されたので"Sub End"という文字列が出力されるようになります。
なお、スレッド間でメモリ空間が共有されると述べましたが、メソッド内のローカル変数などは通常通り共有されません。したがって、複数のスレッドが同一のメソッドを呼び出したときにローカル変数を共有するのではないか、といった心配は必要ありません。
課題
ShareTestをいじってスレッド1とスレッド2が鬼ごっこをするプログラムを作れ。
一辺50ドットとの正方形の2次元空間を移動するものとする。
共有する変数は配列にするとよい。
参考Webページ
Javaの道