スレッド
スレッドとは
今までのプログラムは流れが1つしかありませんでした。しかし、処理の流れが1つでは時間のかかる処理を実行するとその間にプログラムはその処理にかかりっきりになり、ユーザからすると停止したように見えて、操作もできなくなってしまいます。
そこで処理の流れを複数にする技術があります。それがスレッドです。スレッドとは処理の流れのことを意味し、今まではスレッドが1つしかなかった状態でした。これを複数のスレッドを同時に動かし、並行して処理を行わせることが出来ます。
サンプル
まずはサンプルとして時間がかかる処理を作ってみます。典型的な例はダウンロードですので、ダウンロードをシミュレーションしたクラスを作ってみます。
public class Download{
String name;
public Download(String name) {
this.name = name;
}
public void down() {
int size = 0;
while(size<100) {
size += 20;
System.out.println(name + ":" + size + "バイト");
}
}
}
これは100バイトをダウンロードすることをシミュレーションしたクラスです。 nameにはダウンロードするファイル名を入れるものとします。 downメソッドでダウンロードしたファイルサイズを加算していきます。ファイルのサイズは100バイトとし、一回につき20バイトダウンロードします。今までダウンロードしたサイズを随時表示します。
mainメソッドでは以下のようにしてfile1とfile2という名前でDownloadクラスのインスタンスを作成し、downで実行しました。
public class DownloadMain {
public static void main(String[] args) {
Download file1 = new Download("file1");
Download file2 = new Download("file2");
file1.down();
file2.down();
System.out.println("処理を終了しました");
}
}
実行例は以下になります。file1をダウンロードし、次にfile2をダウンロードしています。
file1:20バイト
file1:40バイト
file1:60バイト
file1:80バイト
file1:100バイト
file2:20バイト
file2:40バイト
file2:60バイト
file2:80バイト
file2:100バイト
処理を終了しました
処理の一時停止
しかし、一瞬で処理が終わってしまうので、時間がかかる処理に見えません。 そこで、Downloadクラスのdownメソッド内で処理を一時停止し、時間がかかるように見せます。 処理を一時停止するには、Threadクラスのクラスメソッド sleep を使います。以下をdownメソッドのwhile内の先頭に書きます。
Thread.sleep(1000);
引数は何ミリ秒停止するか、です。1000を指定すると1000ミリ秒、つまり1秒停止します。
ただし、このメソッドはチェック例外InterruptedExceptionを発生する可能性があるので、try~catchします。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
これで実行するとまずfile1を1秒ごとに20バイトずつダウンロードし、次にfile2を1秒ごとに20バイトずつダウンロードします。それぞれ5秒かかっていますので、計10秒時間がかかっています。
Threadクラス
現在はfile1をダウンロードした後にfile2をダウンロードしています。これをそれぞれ並行してダウンロードさせれば同時に処理を行いますので5秒で処理を終了します。
その並列処理を行うのがスレッドです。
スレッドを使うためには方法が2つあります。まずはThreadクラスを使う方法です。 Threadクラスを継承して、runメソッドをオーバーライドし、その中で別スレッドとして実行させたい処理を書きます。
今回は、DownloadクラスをThreadクラスから継承するようにします。
public class Download extends Thread{
そして、別スレッドとして動かしたい処理はdownメソッドですので、これをrunという名前に変えます。
public void run() {
あとは、mainの方で実行する際に、runメソッドを直接呼び出すのでは無く、start()を呼び出します。これで別スレッドが生成され、runを動かしてくれます。
file1.start();
file2.start();
これで完成です。実行すると以下のようになります。
処理を終了しました
file2:20バイト
file1:20バイト
file2:40バイト
file1:40バイト
file2:60バイト
file1:60バイト
file1:80バイト
file2:80バイト
file1:100バイト
file2:100バイト
いきなり、「処理を終了しました」が表示されます。これはmainメソッドのスレッドが先に終了するからです。その後、file1とfile2がそれぞれrunメソッドを並行して動かしています。そのため、file2とfil2でどちらが先に動くかはそのときによって異なります。
file1とfile2のrunメソッドが終了してプログラムは終わります。並行して動いているのは全てが終わるのは5秒ほどしかかかりません。
スレッドの使い方1
- Threadクラスを継承する
- void run() メソッドを作る
- start()で開始する
Runnableインタフェース
既に何かのクラスを継承しているクラスにスレッド機能を付け加えようとした場合、さらにThreadクラスを継承する、ということはできません。Javaでは多重継承は出来ないからです。
そこで、別の方法としてRunnableインタフェースを実装する、という方法があります。 実はThreadクラスもRunnableインタフェースを実装しています。
まず、Runnableインタフェースをimplementsします。
public class Download implements Runnable{
そして、Threadクラスと同じく、runメソッドを実装します。Runnableインタフェースはrunメソッドを実装する必要があるインタフェースです。
public void run() {
mainの方で実行する際には、まずThreadクラスのインスタンスを作らなくてはなりません。このとき、コンストラクタでRunnableインタフェースを実装したクラスを指定します。
Thread th1 = new Thread(file1);
Thread th2 = new Thread(file2);
あとは、そのオブジェクトでstartを行います。
th1.start();
th2.start();
スレッドの使い方2
- Runnableインタフェースを実装する
- void run() メソッドを実装する。
- Threadクラスのインスタンスを作り、コンストラクタでオブジェクトを指定。
- start()で開始する。
スレッドの制御
Thread.sleepのようにThreadクラスにはスレッドクラスを制御するメソッドがいくつかあります。
yield
sleepは処理を一時停止します。その間に他のスレッドが動作することが出来ます。 このように処理の実行権を譲り渡すことができるのが yieldです。sleep(0)と似た働きをします。
Thread.yield();
stop
スレッドを実行時、強制的に停止するのがstopメソッドです。 しかし、これは強制的にスレッドが打ち切られるため使用が推奨されないメソッドです。
スレッドの制御まとめ
- sleep でスレッドを休止
- yield でスレッドの実行権を他に譲る
- stop でスレッドの強制終了(推奨されない)
排他制御
排他制御無しの場合
例としてダウンロードの合計サイズを保持するクラスDownSumを作成したとします。 addメソッドで合計サイズを足していきます。分かりやすいように足す前(旧)と足した後(新)を表示しています。
public class DownSum {
private int sum;
public DownSum() {
this.sum = 0;
}
public void add(int size) {
int s = this.sum;
System.out.println("旧:" + s + "バイト");
s += size;
System.out.println("新:" + s + "バイト");
this.sum = s;
}
}
Dowloadクラスに staticフィールド でこのDownSumのインスタンスを生成しておきます。runメソッドでは、サイズをaddメソッドを使って渡します(runではsleepは削除しておきます)。
class Download implements Runnable{
String name;
static DownSum ds = new DownSum();
public Download(String name) {
this.name = name;
}
public void run() {
int size = 0;
while(size<100) {
size += 20;
System.out.println(name + ":" + size + "バイト");
ds.add(20);
}
}
}
これを実行すると2つのDownloadクラスで100バイトダウンロードしますから計200バイトになるはずです。しかし、異なるサイズが出てしまいます。
旧:120バイト
新:140バイト
なぜそうなるのか
問題はDownSumクラスのaddメソッドにあります。
public void add(int size) {
int s = this.sum;
System.out.println("旧:" + s + "バイト");
s += size;
System.out.println("新:" + s + "バイト");
this.sum = s;
}
スレッドが複数ある場合、並列して処理が動いていきます。 変数 s に this.sum の値を代入した後、それにsumを加えてから、this.sumに戻すまでの間に、他のスレッドが、this.sum を参照してしまうと、元の数字のままです。つまり、お互いにthis.sumを上書きしてしまいます。
int s = this.sum;
System.out.println("旧:" + s + "バイト");
s += sum;
System.out.println("新:" + s + "バイト");
// もし、ここでthis.sumを他スレッドに参照されると、元の数字のまま
this.sum = s;
synchronized
これを回避するにはメソッドの前にsynchronizedと付けます。これにより、このメソッドは同期メソッドとなり、処理が終わるまでは他のメソッドからはこのメソッドが使えません。
public synchronized void add(int size) {
int s = this.sum;
System.out.println("旧:" + s + "バイト");
s += size;
System.out.println("新:" + s + "バイト");
this.sum = s;
}
実行すると、正しい結果になります。
旧:180バイト
新:200バイト
なお、synchronizedはメソッド全体に付けるのでは無く、ブロックの前に指定することも出来ます。このときには、synchronized(オブジェクト)のように指定します。
public void add(int size) {
synchronized(this) {
int s = this.sum;
System.out.println("旧:" + s + "バイト");
s += size;
System.out.println("新:" + s + "バイト");
this.sum = s;
}
}
排他制御のまとめ
- synchronizedをメソッドに付ける
- synchronizedをブロックに付けて、対象オブジェクトを指定する