javaのプログラムは並列処理を行う為のクラスThreadを用意しています。
全てのクラスの継承元となる Objectクラスにはスレッドに関連したメソッドが定義されています。
GUIなどのイベントシステムでは複数のスレッドが走る。 スレッドの理解はこれからのソフトウエアを理解する為には必須です。
[目次 ]
計算機のプログラムは機械語の命令を順番に並べたものです。CPUが命令を順番に読んで実行すると、プログラムの目的が達成されるように書かれています。ここでCPUが実行する順番に命令を辿って行けば、1本の線ができます。この線を"thread of execution"と いいスレッドと呼んでいます。
複数の処理を同時に実行することを並列処理と言います。1個のCPUは同時に複数の処理は実行できないので、複数のCPUが必要になります。メモリーアクセスや入出力なども競合する可能性があるのでCPUごとにメモリーや入出力まで持つ必要があるかもしれません。厳密 な同時性を要求すると、エクセルとワードを同時に使いたい時はパソコンを2台用意しなければなりません。
※現在、1つのCPUの中に複数のCPUコアを持つLSIが使われるようになり、CPUの中での並列処理が実現されています。エクセルとワードを同時に実行する場合も、実行するコアを分けることで物理的な並列処理が可能になりました。ただし、メモリーの読み書き部分の競合がまだ問題です。
複数プログラムのスレッドを短い時間で行き来して、見かけ上同時に処理する手法を時分割処理と言います。
スレッドの移動は、CPUが現在のスレッドを継続して実行するために必要な情報を保存してから、別スレッドを継続実行するための情報をレジスタに読み込むことで実現します。
手間はかかりますが、見かけ上の並列処理が可能になる利点は大きく。1台の大型計算機を大勢で利用できるTSS(TimeSharingSystem)は大型計算機には必須の機能です。現在はパソコンもCPUが高性能化しTSSによる並列処理機能がOSの基本機能として用意されています。
並列処理では使用するメモリーの競合が起こるかもしれません。たとえば、同じメモリーに2つのスレッドが書き込みを行うと、先に書き込んだデータは上書きされて消えてしまいます。この問題は、スレッドが使用できるメモリー領域を重ならないように分けることで解決できます。
スレッドごとに専用のメモリー領域を用意 したものをプロセス(タスク)と言います。複数プロセスの並列実行はOSの重要な機能で、この機能をマルチタスク(Multi Tasking)と言います。たとえば、Windowsではエクセルとワードを同時に起動して使うことができます。このときWindowsはエクセルとワードの2つの プロセスを並列実行します。
マルチタスクは処理の競合を避けるにはいい方法です 。しかし、使えるメモリーが分けられているため、プロセス間のデータ受け渡しは、プロセスの外、例えば通信等で行う必要があり、手間がかかります。
例えば、Windowsで実行中のエクセルとワードでデータを移動させるにはクリップボード等に一旦コピーしてから目的の場所に張り付けるなどしなければなりません。
1つのプログラムの中で複数の処理を並列に実行したい場合もあります。1個のプロセス内に複数のスレッドを持つものをマルチスレッド・プログラムと言います。 マルチスレッドではメモリーを共有するので、競合の問題は発生しますが、スレッド間のデータ受け渡しは容易です。
簡単なC言語のプログラムはCPUがmainメソッドから命令を順番に実行するシングルスレッドのプログ ラムです。
javaプログラムもmainメソッドから実行開始され、この処理の流れをメインスレッドと呼びます。しかし、javaにはガベージコレクショ ン:GCを行う機能が有ると紹介してきました。実は、このGCの処理は別スレッドとして実行されています。従ってjavaのプログラムが実行されている状況は初めからマルチスレッドになっています。
さらに、java言語ではプログラムに新しいスレッドを追加する仕組みが用意されています。これを利用すると多数のスレッドを使 うプログラムを容易に作ることができます。
javaプログラムのように同じプロセスの中でマルチスレッ ドの処理を行うとメモリーや入出力で競合の問題が起きます。例えば同じ変数の読み書き、同じ画面への書き出し、同じファイルの読み書き等です。
従って、複数のスレッドが同じものを同時に使わないようにする仕組みが必要です。方法は複数ありますが、javaではロック と呼ばれる仕組みが用意されています。
ロックはオブジェクトに鍵をかける(ロックする)ことで他のスレッドが同じオブジェクトをロックできないようにする仕組みで す。
スレッドはオブジェクトをロックできないと実行を中断して待ち状態となり、ロックが解除されるのを待ちます。
[目次 ]以下にThreadを使ったプログラムの例を示します。
例題ではボタンごとにスレッドが作られ文字をカウントアップしています。各スレッドの休み時間の違いで数字の増える速度が異なります。
Cells.jar
Threadのオブジェクトは新しいスレッドを開始し、このスレッドでコンストラクタの引数で渡された オブジェクトのrunメソッドを呼び出します。 この呼び出しから戻るとスレッドは終了します。
/*Cells.java*/
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class Cells extends Frame
{ //アプリケーションでの動作テスト用main //アプレットとして動くときは使われない
static public void main(String args[])
{
Cells frame=new Cells();
frame.setSize(300,100);
frame.setVisible(true); frame.addWindowListener(frame.new Control());
}
public Cells()//組み立て
{
setLayout(new GridLayout(1,5));
for(int i=0;i<5;i++){
Cell cell=new Cell(i);
add(cell);
cell.start();
}
} class Control extends WindowAdapter{ public void windowClosing(WindowEvent e){System.exit(0);} }
}
/*
スレッドを使って実行されるオブジェクトのクラス
*/
class Cell extends Button implements Runnable { //スレッドの休み時間 int sleepTime; int n=0; Cell(int n) { sleepTime=200; for(int i=0;i<n;i++) sleepTime*=2;//休み時間の設定 } //スレッドを実行するオブジェクト Thread thread=null; //Threadを作成しスタート //下のrunがスレッドから1回だけ呼ばれる void start() { if(thread==null){//startを2回呼ぶときの用心 thread=new Thread(this);//Threadを作成 thread.start();//Threadをスタート
}
}
//スレッドを終了する
//変数threadをnullにするとrunのwhileループが終了する
void stop()
{
thread=null;
}
//Runnableの実装部分 //スレッドから1回だけ呼ばれる。 //実行がrunがら戻ると終了する。 public void run() { while(thread!=null){ //thread!=nullの間は繰り返す
//ここにスレッドにさせたい仕事を記述
n++;
setLabel(""+n);
repaint();
//他のスレッドを実行する時間を与える為にここで少し休む
//CPU時間を必要以上に独占しないようにする。
try{Thread.sleep(sleepTime);}catch(Exception e){}
} } }
Threadは自分のrunを新しいスレッドで実行するので、これを上書きする方法です。
ThreadはコンストラクタでRunnableを実装したオブジェクトが渡されると、渡されたオブ ジェクトのrunを実行します。上の例題11aはこの方法で書いています。
※プログラムは1)より面倒ですがjavaは単一継承なので、インターフェイスを実装する方が自由度が高い。
この例題のA,Bボタンは押されると新しいスレッドを作り1行の書き出しを繰り返し行う。ボタンは2回押すと止まる。
ここでA,B両方のボタンが押された状態では文字の書き出しが混ざってしまう。これは書き出しについて全く制限が無いために起きている。
Cells2.jar
/*Cells2.java*/
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class Cells2 extends Frame
{
static public void main(String args[])//テスト用
{
Cells2 frame=new Cells2();
frame.setSize(300,200);
frame.setVisible(true); frame.addWindowListener(frame.new Control());
}
public Cells2()
{
TextArea text=new TextArea();
add("Center",text);
Panel panel=new Panel();
panel.setLayout(new GridLayout(1,2));
panel.add(new Cell2("A",text));
panel.add(new Cell2("B",text));
add("North",panel);
} class Control extends WindowAdapter{ public void windowClosing(WindowEvent e){System.exit(0);} }
}
class Cell2 extends Button implements Runnable,ActionListener
{
//スレッドの休み時間
int sleepTime=100;
String s="?";
TextArea text;
Cell2(String s,TextArea text)
{
super(s);
this.s=s;
this.text=text;
addActionListener(this);
}
public void actionPerformed(ActionEvent e)
{
if(thread==null)start();
else stop();
}
//スレッドを実行するオブジェクト
Thread thread=null;
//Threadを作成しスタート
//下のrunがスレッドから1回だけ呼ばれる
void start()
{
if(thread==null){
thread=new Thread(this);
thread.start();
}
}
//スレッドを終了する
//変数threadをnullにするとrunのwhileループが終了する
void stop()
{
thread=null;
}
//Runnableの実装部分
//スレッドから1回だけ呼ばれる。
//実行がrunがら戻ると終了する。
public void run()
{ setLabel("run");
while(thread!=null){ //thread!=nullの間は繰り返す
//ここにスレッドにさせたい仕事を記述
printline();
//他のスレッドを実行する時間を与える為にここで少し休む
//CPU時間を必要以上に独占しないようにする。
try{Thread.sleep(sleepTime);}catch(Exception e){}
}
setLabel(s);
}
//runの中から繰り返し呼び出される
//文字を20個書いて改行する
//同じtextに書き出す為に競合する void printline() { for(int i=0;i<20;i++){ text.append(s); System.out.print(s);/*確認と時間稼ぎ*/ } text.append("\n"); System.out.println("");/*改行*/ } }
文字が混ざるのを解決する為には競合している資源textをロックしてから書き出しを行えばいい。
オブジェクトをロックする構文は次のように2種類用意されている。
1)synchronized(ロックするオブジェクト){ ロックできたら行う処理 }
2)synchronizedを付けてメソッドを定義する。
メソッドを持っているオブジェクトをロックできたらメソッドを実行する。
Cells2a.jar
/*Cells2a.java*/
//runの中から繰り返し呼び出される
//文字を20個書いて改行する
void printline()
{
synchronized(text){//textをロックできたらブロック内を実行する for(int i=0;i<20;i++){ text.append(s); System.out.print(s);/*確認と時間稼ぎ*/ } text.append("\n"); System.out.println("");/*改行*/ } }
※この例では2)の方法は使えないprintlineメソッドを以下のように書換 えても、これはボタンをロックするだけでテキストエリア(text)の競合を解決できない
//runの中から繰り返し呼び出される
//文字を20個書いて改行する
synchronized void printline() //このメソッドのあるオブジェクトをロックできたら { //メソッドのブロックを実行する for(int i=0;i<20;i++){ text.append(s); } text.append("\n"); }
[目次 ]
2つの資源A,Bを2つのスレッド1,2が共にロックしようとして、スレッド1はAをロック、スレッド2はBをロックした場合何が起きるでしょうか?。 スレッド1は次はBをロックしようとしてロックできず、スレッド2がBのロックをはずすのを待ちます。しかし、スレッド2もAをロックしようとしてスレッド1がAのロックをはずすのを待っています。 結果は永遠に待ち続けることになってしまいます。
ロックによる競合解決は、このようなデッドロックを引き起こす可能性が在ります。
並列処理ではこのようは競合の問題が色々あり、さまざまな解決方法が提案されています。しかし、万能な方法があるわけではありません。
[目次 ]synchronizedでロックしたオブジェクトについてはwaitとnotifyメソッドが呼べます。
※これらのメソッドはObjectで定義されているので、 全てのオブジェクトに存在する。
waitメソッドを呼んだスレッドは実行を一時停止します。停止中はスレッドによるロックは解除されます。
※このメソッドを呼ぶにはwaitを呼ぶオブジェクトのロックをスレッドが持っている必要があります。
synchronized ( obj ){
while(条件が不成立なら)obj.wait();//条件が成立するまで繰り返しwaitして待つ
//条件成立後実行する処理をここに書く
}
※スレッドが次の命令を実行するのを止めて待つ方法には、無駄なループで時間を稼ぐ方法
while(条件が不成立なら){時間稼ぎの処理;}
が在りますが、これはCUPに無駄な作業をさせ続けることになるので止めましょう。waitはスレッド自体を止めて待つため、CPUに負荷をかけません。
waitしているスレッドを再開します。notifyはwaitしているスレッドをどれか1個だけ再開します。wait中のスレッドが沢山あるなら、notifyAllを呼べば全てを再開します。
再開したスレッドは解放していたロックを再取得します。
synchronized ( obj2 ){[目次 ]
//条件を変更する処理をの後で
obj2.notifyAll(); }
上に書いた以外にも、マルチスレッドのプログラムで注意しなければいけないことは幾つかある。
プログラムがコンパイルされるときにさまざまな最適化が行われます。ここで、シングルスレッドのプログラムであることを前提にした最適化が行われるとマルチスレッドでは問題が起きる結果となる場合があります。たとえば、主メモリーの参照は時間がかかるので変数の値をレジスタに置いて、計算をレジスタの上で進め計算が 一段落したところで、主メモリーに書き戻すような処理がよく行われます。スレッドごとに変数の一時コピーをレジスタに作って計算し最後に結果だけを主メモリーに書き戻すことになれば、同じ変数のはずなのに各スレッドが参照する値が異なることも起こります。volatile修飾子を変数に付けることで、最適化においてその変数の値が別のスレッドにより変更される可能性を考慮することを要求します。この結果、スレッドごとに変数の一時コピーを作らないなどの最適化の抑制が行われます。
複数のスレッドがあるとき、優先してCPU時間を割り当てる必要がある場合は優先順位を高く設定します。優先順位の変更はThreadクラスの定数を使ってThreadのメソッドsetPriorityを呼び出すことで可能です。
public final void setPriority(int newPriority)
setPriorityに渡す値は以下の3種から選んでください
Threadのオブジェクトは新しいスレッドでrunメソッドを実行し、runから戻るとスレッドを終了します。多くの場合、例題プログラムに書いたようにフラグ変数を操作してrunメソッドから戻るようにプログラムを書きます。
スレッドを停止させるstopメソッドがThreadオブジェクトに存在していますが、これを使うことは推奨できないとされています。
[目次 ]
時刻を表示するウィンドウを作ってください。Frameを継承したクラスの名前はP10です。.レポートツールの初期ファイルはP10.java です。
クラスP10はFrameを継承し、Runnableを実装して作る。
Threadを使った並列処理で表示する時刻を書き換えるのでThreadの使い方を思い出す。
時刻の値は国によって違います。ここで日本時間としてしまうと、他の国では変な時計になってしまいます。このような国や地域を区別するインスタンスがjava.util.Localeです。
まずプログラムを実行する環境の既定のLocaleを取得し、これを元にその地域の歴(java.util.Calendar)のインスタンスを準備します。これをオブジェクトのフィールドに保持しましょう。
さらに時刻はSystem.currentTimeMillis( )でミリ秒単位の現在時刻の値が返されるので、これを使うことにします。ここで前回の表示時刻を秒単位の値でフィールド t0 で 持つことにします。これは次の表示の書き換え等で無駄な描画を避けるためです。
Calendar carendar=Calendar.getInstance(Locale.getDefault()); long t0=-1;//t0は過去に描画した時の秒針の値を覚えておくためのフィールド .......
run( )で繰り返し画面の再描画のrepaint( )メソッドを呼ぶことで時計の値を書きなおしますが無駄に書き直すのは良くないので、秒針の値が変わった時だけ書き直すことにします。
public void run() { while(thread!=null){ long t=System.currentTimeMillis()/1000;//ミリ秒単位なので千で割って秒単位に if(t!=t0) {//秒が変わったときだけ再描画 t0=t; repaint(); } try{ Thread.sleep(100);//0.1秒休む }catch(Exception e){/*例外でも何もしない*/} } }
描画メソッドで時刻の文字列を描画するのですが、ここでまずは文字列を作らなければなりません。ここで暦法に従って時分秒の値を求め、この値から時刻表示のフォーマットに合わせた文字列を作り、そして...
public void paint(Graphics g)//表示だけ上書き { carendar.setTimeInMillis(System.currentTimeMillis());//現在時刻をセット int h=carendar.get(Calendar.HOUR_OF_DAY);//24時間制の時間を取得 int m=carendar.get(Calendar.MINUTE);//分を取得 int s=carendar.get(Calendar.SECOND);//秒を取得 //C言語のprinntfみたいな書式付き文字列生成が可能になっています String text=String.format("%02d:%02d:%02d\r\n",h,m,s); ......表示する文字の大きさを設定し、..... ...... 描画する ..... }
※文字フォントの大きさの変え方は、「9.継承と実装」の例題9bを参照.