特集PC技術

Java言語入門 ~C言語を学んだ君へ~

 

[第14回]マルチスレッド

この回ではマルチスレッドについて学習しましょう。

目次

[1] マルチスレッドとは

今までやってきたプログラムは、シングルスレッドと呼ばれています。
何か処理を行いたい時には、順番に行われるように指示をしてきました。
しかし、以下のような状態になった場合にはシングルスレッドのままでは大丈夫でしょうか。

以下のような場面があったとします。
受付が1つしかない、病院でたくさんの人がやってきたとします。
そうなると前の人が終わるまで次の人は待つ必要があります。
これでは、後ろの人はたくさん待たされることになり、とても不満が出ることでしょう。
その対策として受付を複数設ければ1つしかない場合に比べてだいぶましになるはずです。
そう、これこそがまさにこれから学習するマルチスレッドなのです。

ところでスレッドとは一体何のことでしょうか。
スレッドとは、パソコンが処理を行う最小単位のことです。
タスクという言葉を聞いたことがあると思います。
スレッドはタスクよりもさらに小さな処理単位です。

では、マルチスレッドプログラムを体験していきましょう。
といってもいきなり始めては分かりにくいでしょうからまずはイメージを見てみましょう。
ある3つの処理があったとします。これをそれぞれ「A」、「B」、「C」とすることにします。

シングルスレッドのイメージ

シングルスレッド

まずは今まで学習してきたシングルスレッドから確認してみます。
A→B→Cとといった具合に順番通り処理をしていきます。

マルチスレッドのイメージ

画像

シングルスレッドでは順番の処理を実行してきました。
マルチスレッドでは、上記のように複数の処理を並行して行ってくれます。
同時に実行することにより効率よく処理ができるようになります。
便利なものには弊害があり、以下のような注意点があります。

マルチスレッドの注意点

今まで1つずつしかできなかった処理が複数同時に実行できるようになります。
とても便利な機能なのですが注意点がいくつかあります。

・ 実行順序に決まりはない
・ 実行のたびに変化する

複数のスレッド間で全く関係がない場合には問題はありません。
しかし、複数のスレッドが同じ変数にアクセスする場合には大変なことになります。
このような時は他の処理から影響を受けないように排他制御を行います。
排他制御は、後で説明をします。

マルチスレッドの実現のために

通常パソコンは1つのCPUを所持しています。
1つのCPUにつき、1つの処理が原則です。
このままでは複数処理なんてすることができません。
では、どのようにしてマルチスレッドを実現しているのでしょうか。
そこで、マルチスレッドを実現するためにCPUは、ある方法をとっています。
CPUを短い時間間隔で実行する処理を切り替えているのです。
これを「時分割処理(タイムシェアリング)」といいます。
複数の処理を短い間隔で頻繁に切り替えることで、
まるで複数の処理を同時に実行するように見せているのです。

[2] マルチスレッドの方法1 Threadクラス

まずはThreadクラスを用いたマルチスレッドを学んでいきましょう。
Threadクラスを使ったマルチスレッドの実行方法は以下のとおりとなります。

Threadクラス

1、Threadクラスを継承してサブクラスを作成
2、サブクラス内にrun()メソッドを記述(オーバーライド)
3、サブクラスをインスタンス化
4、オブジェクトのstart()メソッドで実行

上記4手順によりThreadクラスを使ったマルチスレッドが実現できます。

1つ目の手順はThreadクラスを継承してサブクラスを作成することです。

class TestThread extends Thread{...}

と書きます。(ここではTestThreadという名前のサブクラスを作成しました。)
継承をもし覚えていなかったら過去の回に戻り復習をしましょう。

次に、サブクラスの中にrun()メソッドを記述します。

public void run(){...}

です。このメソッド内に記述された処理がマルチスレッドを実行した時に行われます。

3つ目はサブクラスをインスタンス化することです。

TestThread Test1 = new TestThread();

その後、mainメソッド内でこのオブジェクトをstart()メソッドで実行します。
では、実際にプログラムで見てみましょう。

マルチスレッド(Thrad)プログラム
public class Java14_01 {
	public static void main(String args[]){
	    TestThread tt1 = new TestThread("スレッド1");
	    TestThread tt2 = new TestThread("スレッド2");
		
	    tt1.start();
	    tt2.start();
		
		try{
			for(int i = 0; i < 5; i++){
			    System.out.println("メイン実行中");
			    Thread.sleep(500);
			}
		}catch (InterruptedException e){
		    System.out.println(e);
		}
		finally{
		    System.out.println("メイン終了");
		}
	}
}

class TestThread extends Thread{
	private String ThreadName;
	
    TestThread(String name){
		this.ThreadName = name;
	}
	
	
	public void run(){
		try{
			for(int i = 0; i < 5; i++){
			    System.out.println(ThreadName+"実行中");
			    Thread.sleep(1000);
			}
		}catch (InterruptedException e){
		    System.out.println(e);
		}
		finally{
		    System.out.println(ThreadName+"終了");
		}
	}
}
実行結果

マルチスレッド実行結果

スレッドをメイン以外に2つ作成しています。
そして、メインと2つのスレッドを同時に実行しています。

例外処理の中に書いてある、sleep()メソッドは、スレッドの処理を指定した時間(ミリ秒)だけ待機させます。
Thread.sleep(時間)と記述します。
Threadクラスのメソッドです。
継承しているサブクラス(このプログラムでは、TestThrad)では、
Thread.sleep(時間)をsleep(時間)と書くこともできます。

[3] マルチスレッドの方法2 Runnableインタフェース

次にRunnableインタフェースを使ったマルチスレッドを学びましょう。
では、Threadの時と同じように手順を見ていきましょう。

Runnableインタフェース

1、implements でRunnableインタフェースを指定してクラスを作成
2、クラス内でrun()メソッドを実装
3、作成したクラスをインスタンス化
4、インスタンス化したクラスをThreadのコンストラクタの引数として渡す
5、start()メソッドで実行

Threadクラスとは違い、1つ余分な手順があります。
Runnableを実装したクラスをインスタンス化した後に そのオブジェクトをThreadのコンストラクタの引数として渡します。

TestRun tr = new TestRun();
Thread t = new Thread(tr);

では、実際にプログラムで見ていきましょう。

マルチスレッド(Runnable)プログラム
public class Java14_02{
	public static void main(String args[]){
	    TestRun tr = new TestRun("サブスレッド");
	    Thread t = new Thread(tr);
		
	    t.start();
		
		try {
			for(int i = 0; i < 5; i++){
			    System.out.println("メインスレッド実行中");
			    Thread.sleep(500);
			}
		}catch (InterruptedException e){
		    System.out.println(e);
		}finally{
		    System.out.println("メイン終了");
		}
		
	}
}

class TestRun implements Runnable{
	private String ThreadName;
	
    TestRun(String name){
		this.ThreadName = name;
	}
	
	public void run(){
		try {
			for(int i = 0; i < 5; i++){
			    System.out.println(ThreadName+"実行中");
			    Thread.sleep(600);
			}
		}catch (InterruptedException ex){
			
		}finally{
		    System.out.println(ThreadName + "終了");
		}
	}
}
実行結果

Runnableの実行結果

RunnableとThreadの動作は変わりません。
ここまででスレッドを使う上での基本的なことを学びました。

[4] ThreadとRunnableの使い分け

さて、動作が同じなので、どのように使い分ければよいのでしょうか。
使い分ける基準としては、

run()メソッドを実装するだけならば、「Runnable」
run()メソッド以外もオーバーライドするならば、「Thread」

と2つを使い分けるとよいでしょう。

これで、マルチスレッドのプログラムはできました。
しかし、このままではできないことがまだあります。
少しだけ説明がありました。そう、排他制御です。
それは何なのか。次で学びましょう。

[5] 排他制御

マルチスレッドはこのままでは、問題があります。
それは、ある処理が終わるまで他の処理をしてほしくない場合に、勝手に処理をしてしまうことです。
同時に実行できるようになったことで素早く処理ができるようになりましたが、そのために問題がでてしまいました。
イメージで説明すると、電車にみんなが一斉に駆け込んでいる状態と同じです。
けがをする危険性もあり、良くありません。
順番に並び電車に入れば危険性が低くなり、電車に乗ることができます。

そのような勝手な処理を防ぐために、排他制御を行います。
排他制御を行うことで順番を守ることができるのです。
プログラムで言うと、複数のスレッドの処理順番を決めることになります。

排他制御を行う方法として、synchronizedがあります。
これはメソッドで使う場合と、文で使う場合の2種類があります。

メソッドと文の比較
種類 範囲
メソッド メソッド内全て
自由に指定


メソッドにつける場合にはメソッド全体に影響を与えます。
文につける場合にはメソッドにつけた場合に比べて細かく範囲を指定できます。
また、2つともstaticがあるかないかで少し異なります。
staticの違い

staticがある場合とない場合の違いは何に対してロックがかけられるかです。
ロックとは、既に利用されていて実行できない状態のことです。
このロックが解除された時に実行できるようになります。
では、2つは何を基準に判断するのでしょうか。以下の表を見てください。

static 排他処理の基準
なし 同じインスタンス
あり 同じクラス

staticがない場合には、ロックがインスタンスに対して設定されています。
このインスタンスを所持しているほうに、実行する権利が与えられます。
ないスレッドは既に所持しているスレッドの実行が終わるまで待ちます。

staticがある場合には、クラスに対して設定されています。
そのクラスが利用されていれば、待つ必要があります。

[6] メソッド(synchronized)

では、メソッドの「synchronized」から見ていきましょう。

メソッドの形式

メソッドで使う場合には次のように書きます。

修飾子 synchronized 戻り値 メソッド名 (引数){...}

次はプログラムの書き方です。
なお、プログラムではstaticを使用しません。

メソッドのsynchronizedプログラム


1、あるクラスのインスタンスを生成
2、作成したインスタンスをスレッドのコンストラクタの引数として渡す
    (スレッドの作成方法は前ページを参照してください)
3、それぞれのスレッドを実行する

staticをつけないと、判断材料はインスタンスになります。
まず判断材料のクラスをインスタンス化し、そのオブジェクトを各スレッドに渡します。

test obj = new test(); test2 tt1 = new test2(obj,);
では、実際にプログラムで動作を確認してみましょう。

サンプルプログラム
class test{
	private static int var1 = 20;
	
	public synchronized void decrease(String name) {
		
		try { 
			    var1--;
			    System.out.print("var1 ="+ var1);
			    Thread.sleep(1000);
				
		} catch(InterruptedException e) {
			    System.out.println(e);
		}finally{
			    System.out.println(" "+name+"の処理が終了");
		}
	}
}

class test2 extends Thread{
    test obj;
    String name;
	
    test2(test obj, String name){
		this.obj = obj;
		this.name = name;
	}
	
	public void run() {
		for(int i = 0; i < 5; i++){
		    obj.decrease(name);
		}
	}
}

public class Java14_03 {
	public static void main(String args[]) {
	    test obj = new test();
	    test2 tt1 = new test2(obj, "A");
	    test2 tt2 = new test2(obj, "B");
	    test2 tt3 = new test2(obj, "C");
	    test2 tt4 = new test2(obj, "D");
		
	    System.out.println("開始");
		
	    tt1.start();
	    tt2.start();
	    tt3.start();
	    tt4.start();
		
		try{
		    tt1.join();
		    tt2.join();
		    tt3.join();
		    tt4.join();
		}catch(InterruptedException e){}
	    System.out.println("終了");
	}
}
実行結果

メソッドのsynchronized実行結果

一定の順番で処理が実行されるようになりました。
main内の、test obj = new test();で、インスタンス化をしています。
インスタンス化した、objを各スレッドの引数として渡しています。
sychronizedを使うことにより順番に処理を実行してくれます。
sychronizedがない場合にはどうなるのか、実際に試してみてください。
それぞれのスレッドが好き勝手に処理をしています。

start()メソッドの後に例外処理がありますが、
この中に書かれているjoin()というメソッドがあります。
これは、各スレッドが終了するまでこの先の処理を行わないというメソッドです。
そのために、最後のSystem.out.println("終了")の文は、全てのスレッドが終了してから実行されます。
tryからcatch文までを、全てコメントアウトなり削除なりして、試してみてください。
先にSystem.out.println("終了")が実行されるはずです。

[7] 文(synchronized)

次に文の場合のsynchronizedを見ていきましょう。

文の形式

synchronized(インスタンス){ 排他処理}

ブロック内「{...}」の処理が複数のスレッドで同時に実行されなくなります。
後は誰が処理する権利を持っているかで実行順序が決まります。
メソッドの場合と違い、プログラマーが自分で任意の範囲を指定できます。

次はプログラムの書き方です。
先ほどのプログラムを少しだけ改良しています。

文のsynchronizedプログラム
class test{
	private static int var1 = 20;
	
	public void decrease(String name, test obj) {
		
		synchronized(obj){
			try { 
				
				    var1--;
				    System.out.print("var1 ="+ var1);
				    Thread.sleep(1000);
				
				 
			}catch(InterruptedException e) {
			    System.out.println(e);
			}finally{
			    System.out.println(" "+name+"の処理が終了");
			}
		}
	}
}

class test2 extends Thread{
    test obj = null;
    String name;
	
    test2(test obj, String name){
		this.obj = obj;
		this.name = name;
	}
	
	public void run() {
		for(int i = 0; i < 5; i++){
		    obj.decrease(name, obj);
		}
	}
}

public class Java14_04 {
	public static void main(String args[]) {
	    test obj = new test();
	    test2 tt1 = new test2(obj, "A");
	    test2 tt2 = new test2(obj, "B");
	    test2 tt3 = new test2(obj, "C");
	    test2 tt4 = new test2(obj, "D");
		
	    System.out.println("開始");
		
	    tt1.start();
	    tt2.start();
	    tt3.start();
	    tt4.start();
		
		try{
		    tt1.join();
		    tt2.join();
		    tt3.join();
		    tt4.join();
		}catch(InterruptedException e){}
	    System.out.println("終了");
	}
}
実行結果

文のsynchronized実行結果

先ほどのソースを少しだけ変更しただけです。
文の場合には、排他処理を行いたい場所を囲うことでできます。
文のほうが、細かく設定できるという利点があります。

[8] マルチスレッドの注意点

ここでは、基本的なことだけを学習しました。
マルチスレッドの注意点は実際に実行される順序が一定でないことです。
プログラムが実行されるたびに変更されます。
そのために、「synchronized」を使って順番を決めるのです。
お互いが処理の順番を守ることで予定していた処理を行うことができます。
しかし、排他処理の場合には間違えてデッドロックになってしまう危険性もあります。

デッドロック

デッドロック

上の図を参考に説明します。
処理は上から下に流れているとします。

そして、2つのスレッドがあります。スレッドAとスレッドBです。
スレッドAがCを所持、スレッドBがDを所持(ロック)していたとします。
このC,Dを所持しているとある処理が行える「優先権がある」と考えてください。
この時にスレッドAはDが解放されるまで待機、それまでCを持っている。
同じようにスレッドBもCが解放されるまで待機、それまでDを持っている。
このような状態が発生してしまうとこれ以上処理が行われなくなってしまいます。
この状態をデッドロックと呼んでいます。
プログラムが複雑になるとマルチスレッドはとても難しいです。

[9]練習問題 第1問

以下の設問について答えなさい。

設問1.スレッドを作成するときに使うクラス名は?

設問2.スレッドを作成するときに使うインタフェース名は?

設問3.スレッドの排他処理を行うために使うものは?

設問4.join()メソッドはどのようなメソッドか?

設問5.スレッドを実行するメソッドは?

設問6.sleep()メソッドの引数に10000を渡したら何秒間待機するか?

[10] 練習問題 第2問

以下に書かれているプログラムがあります。
これを、Runnableを使ったプログラムに書き直しなさい。

問題のプログラム


14練習問題2プログラム

第2問解答

コメント

匿名

排他処理のところに書いてあるコードを実行しても、実行結果が規則的になりません。

2016年6月28日 12:31

コメントの投稿


画像の中に見える文字を入力してください。

トラックバックURL

http://www.isl.ne.jp/cgi-bin/mt/mt-tb.cgi/1087

pagetopこのページの先頭へ戻る