17 章: スレッド

スレッドとは一つの処理の流れを示す単位.

java は複数のスレッドを同時実行させて並列処理 (マルチスレッド) を可能にする.

[マルチスレッド] は [マルチプロセス] に比べ並列処理を制御する負荷は軽くなるが, メモリ内の変数などに対して排他制御等の考慮が必要になってくる.

スレッドのライフサイクル

スレッドは下図の状態を経て処理が遂行されている.

_images/java-thread017.gif

新規作成:

スレッドのインスタンスが生成 (new) された状態.

実行可能状態:

スレッドが起動 (start メソッド) されたときこの状態になる. JavaVM のスケジューラーがスレッドの優先順位に基づいて, スレッドを実行状態に遷移させていく.

実行状態:

CPU 資源を使用して処理を行っている状態.次の三つの場合により遷移先が異なる.
1. より優先順位の高いスレッドが来たとき. [実行可能状態] に遷移する.
2. wait(), sleep() メソッドが実行されたときや入出力等に伴う処理の遅延が発生したとき, その他, 排他制御でロックされているとき [待機状態] に遷移する.
3. 処理が完了したとき [終了] となる.

待期状態:

実行処理を遂行できない間はこの状態になる. 処理が再開できるようになれば [実行可能状態] に遷移する.

終了:

スレッド処理が終了した状態.

実装方法

スレッドの実装方法は 2 種類がある:

1. [java.lang.Thread] クラスを継承する
2. [java.lang.Runnable] インタフェースを実装する.
  1. [java.lang.Thread] クラスの継承

[java.lang.Thread] クラスを継承して, スレッド処理を実装するには次のような書式になる:

class クラス名 extends Thread{
  public void run(){
    // スレッドの処理を記述する
  }
}

上記 run メソッド (スレッドの処理) を呼び出す方法は次の通り:

Thread thread = new 上記クラス();
thread.start();

Sample1701.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Sample1701Thread extends Thread{
    public void run(){
	// ここにスレッドの処理を記述する
	System.out.println("スレッド処理です");
    }
}

public class Sample1701{
    public static void main(String[] args){
	Thread thread = new Sample1701Thread();
	thread.start(); // スレッドの起動
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1701
スレッド処理です
  1. [java.lang.Runnable] インタフェースの実装

[java.lang.Runnable] インタフェースを実装して, スレッド処理を実装するには次のような書式になる:

class クラス名 implements Runnable{
  public void run(){
    // スレッドの処理を記述する
  }
}

上記 run メソッド (スレッドの処理) を呼び出す方法は次の通り:

Runnable runnable = new 上記クラス名();
Thread thread = new Thread(runnable);
thread.start();

Sample1702.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Sample1702Runnable implements Runnable{
    public void run(){
	// ここにスレッドの処理を記述する
	System.out.println("スレッド処理です");
    }
}

public class Sample1702{
    public static void main(String[] args){
	Runnable runnable = new Sample1702Runnable();
	Thread thread = new Thread(runnable); // [Runnable] を実装したクラスのインスタンスを引数に指定する
	thread.start(); // スレッドの起動
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1702
スレッド処理です

マルチスレッド

複数のスレッドを同時処理させることを [マルチスレッド] といい, これによりスレープット (処理結果の応答速度) の向上が期待できる.

但し, マルチスレッド処理を行えば, すべての事象において必ずその成果がみられる訳ではない. まずマルチスレッド処理の仕組みを考え, どのような時にマルチスレッド処理が有効なのかを知る必要がある.

マルチスレッド処理は各スレッド (=各処理) に対して CPU の資源をタイムライス (時間割) という技術を使って分け合うことで実現している.

例えば, [処理A], [処理B], [処理C] と処理していく場合, 次のようなイメージになる.

_images/java-thread025.gif

シングルスレッドでは [処理A], [処理B], [処理C] と順に処理しているのに対して, マルチスレッドでは処理を細かく分割して, 先の処理が終了するのを待たずに次々処理を行っている. これによりあたかも並列処理をしているかのように見えるわけ.

しかしこのままのイメージだと, 複数のスレッドが CPU を時分割で使用したところで, 各処理の終了する順番が変わることがあってもスループット (応答速度) は変わらない. むしろ処理分割する分は遅くなる!

それではどのようなときにマルチスレッドが有効なの? それは次のように各処理の負荷の差が大きい場合.

_images/java-thread034.gif

上記の場合, シングルスレッドでは [処理A] が終了するまで [処理B], [処理C] は処理されない, つまりシステムの応答が帰ってこない. 従って負荷の高い処理が行われるといちいち待たされるような感じになる.

これに対してマルチスレッドの場合, 負荷の大きい処理が先に起動しても, その終了するのを待つことなく次々と軽い処理が先に終了して, 結果を返すことができるため, スループットが向上する.

注意:

以上の結果からマルチスレッド処理とは, 高負荷の処理が入っても応答を止めることなく, 次の処理を進めたい場合に有効な技術といえる.

同期処理

マルチスレッド処理では各スレッドは独立で動作しているので, 各処理が終了する順番は保証されない. 次のプログラムを使って実際の動作を確認する.

Sample1703.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Sample1703 extends Thread{
    public static void main(String[] args){
	System.out.println("親スレッドが開始した");
	
	Thread thread1 = new Sample1703();
	Thread thread2 = new Sample1703();
	Thread thread3 = new Sample1703();

	thread1.start(); // 子スレッド 1 を起動
	thread2.start(); // 子スレッド 2 を起動
	thread3.start(); // 子スレッド 3 を起動

	System.out.println("親スレッドが終了した");
    }

    public void run(){
	System.out.println("子スレッド: " + this.getName() + " が開始した");
	System.out.println("子スレッド: " + this.getName() + " が終了した");
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1703
親スレッドが開始した
子スレッド: Thread-1 が開始した
子スレッド: Thread-1 が終了した
子スレッド: Thread-2 が開始した
子スレッド: Thread-2 が終了した
親スレッドが終了した
子スレッド: Thread-3 が開始した
子スレッド: Thread-3 が終了した

実行結果から, 実際に処理を記述した順番と各スレッド処理の終了する順番が同じではないことがわかる.

しかし, 処理によってはスレッドの結果を参照するなど, 同期をとる必要がある場面もあるかと思う. そのような場合には:

[java.lang.Thread] クラスのインスタンスメソッド [join()] を使用する

これにより呼び出しもとのスレッドは呼び出したスレッドが終了するまで待機するようになる. 書式は次の通り:

public final void join() throws InterruptedException

上記の例で, 各子スレッドが終了してから親スレッドが終了するように変更すると, 次のようになる.

Sample1704.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Sample1704 extends Thread{
    public static void main(String[] args){
	System.out.println("親スレッドが開始");
	Thread thread1 = new Sample1704();
	Thread thread2 = new Sample1704();
	Thread thread3 = new Sample1704();

	thread1.start();
	thread2.start();
	thread3.start();

	try{
	    thread1.join(); // thread 1 の終了を待つ
	    thread2.join(); // thread 2 の終了を待つ
	    thread3.join(); // thread 3 の終了を待つ
	    System.out.println("親スレッドが終了");
	}catch(InterruptedException e){
	    e.printStackTrace();
	}
    }

    public void run(){
	System.out.println("子スレッド: " + this.getName() + " が開始");
	System.out.println("子スレッド: " + this.getName() + " が終了");
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1704
親スレッドが開始
子スレッド: Thread-1 が開始
子スレッド: Thread-1 が終了
子スレッド: Thread-2 が開始
子スレッド: Thread-2 が終了
子スレッド: Thread-3 が開始
子スレッド: Thread-3 が終了
親スレッドが終了

排他制御

マルチスレッド処理中, 各スレッドが共通のオブジェクトを使用する場合などで思わぬ事態 (データ破壊) が起こることがある.

実際の例として, 座席予約システムを行うサンプルプログラムで検証する. 処理の流れは次の通りである.

  1. 共通オブジェクトとしてシート (Sample1705Sheet) がある. 二つのスレッドがシートの予約処理 (reserve メソッド) を呼び出す.
  2. 予約処理が呼ばれると, まずシートの内部変数 (vacant) で予約が可能かどうかを判断する.
  3. シートが予約されていなければ (vacant = true) 予約処理を行い, 内部変数を予約済みに更新 (vacant = false) して, そのあと [予約が完了した] を表示し終了する. もし, 予約がされていれば [予約済みである] と表示して終了する.

Sample1705.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Sample1705Sheet{
    private boolean vacant = true;

    public void reserver(String name){
	try{
	    if(vacant == true){ // 予約可能時
		System.out.println(name + " が予約確認: シート予約可能");
		System.out.println(name + " の予約処理中...");
		Thread.sleep(1000);
		vacant = false;
		System.out.println(name + "のシート予約が完了");
	    }else{ // 予約不可時
		System.out.println(name + " が予約確認: 予約済み");
	    }
	}catch(InterruptedException e){
	    e.printStackTrace();
	}
    }
}

public class Sample1705 extends Thread{
    static Sample1705Sheet sheet;

    public static void main(String[] args){
	sheet = new Sample1705Sheet();
	Thread thread1 = new Sample1705();
	Thread thread2 = new Sample1705();
	thread1.start();
	thread2.start();
    }

    public void run(){
	sheet.reserver(this.getName());
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1705
Thread-1 が予約確認: シート予約可能
Thread-1 の予約処理中...
Thread-2 が予約確認: シート予約可能
Thread-2 の予約処理中...
Thread-1のシート予約が完了
Thread-2のシート予約が完了

実行結果を見ると一つのシートに対して二つのスレッドが共に予約処理を完了させている. つまり最初のスレッドの予約が破棄されてしまっている. (データ破壊発生)

この例は最初のスレッドが予約処理中 (シートの内部変数を予約済みに変更する前) に別のスレッド予約処理を行ってしまったのが原因である.

次のイメージになる.

_images/java-thread041.gif

従ってこの場合, 正しく処理を行うためには一つのスレッドが予約処理 (reserve メソッド) 中のときは, 他のスレッドは予約処理ができないように排他制御をする必要がある.

synchronized ブロックと synchronized メソッド

排他制御は [synchronized ブロック] または [synchronized メッソド] にて行う. これにより一つのインスタンスに対して処理できるのは一つのスレッドであることが保証される. それぞれの書式は次の通りである.

  1. synchronized ブロック:
synchronized{ 排他制御を行う処理 }
  1. synchronized メソッド:
[アクセス修飾子] synchronized メソッドの戻り値 排他制御させるメソッド(){}

[Sample1705.java] の予約処理に対して排他制御を行う:

1. synchronized ブロック
2. synchronized メソッド

[synchronized ブロック] での排他処理 [Sample1706.java]

Sample1706.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Sample1706Sheet{
    private boolean vacant = true;
    
    public void reserve(String name){
	try{
	    synchronized(this){
		if(vacant = true){
		    System.out.println(name + " が予約確認: シート予約可能");
		    System.out.println(name + " の予約処理中...");
		    Thread.sleep(1000);
		    vacant = false;
		    System.out.println(name + " のシート予約が完了");
		}else{
		    System.out.println(name + " が予約確認: 予約済み");
		}
	    }
	}catch(InterruptedException e){
	    e.printStackTrace();
	}
    }
}

public class Sample1706 extends Thread{
    static Sample1706Sheet sheet;

    public static void main(String[] args){
	sheet = new Sample1706Sheet();
	Thread thread1 = new Sample1706();
	Thread thread2 = new Sample1706();
	thread1.start();
	thread2.start();
    }

    public void run(){
	sheet.reserve(this.getName());
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1706
Thread-1 が予約確認: シート予約可能
Thread-1 の予約処理中...
Thread-1 のシート予約が完了
Thread-2 が予約確認: シート予約可能
Thread-2 の予約処理中...
Thread-2 のシート予約が完了

[synchronized メソッド] での排他処理 [Sample1707.java]

Sample1707.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Sample1707Sheet{
    private boolean vacant = true;
    
    public synchronized void reserve(String name){
	try{
	    if(vacant = true){
		System.out.println(name + " が予約確認: シート予約可能");
		System.out.println(name + " の予約処理中...");
		Thread.sleep(1000);
		vacant = false;
		System.out.println(name + " のシート予約が完了");
	    }else{
		System.out.println(name + " が予約確認: 予約済み");
	    }
	}catch(InterruptedException e){
	    e.printStackTrace();
	}
    }
}

public class Sample1707 extends Thread{
    static Sample1707Sheet sheet;

    public static void main(String[] args){
	sheet = new Sample1707Sheet();
	Thread thread1 = new Sample1707();
	Thread thread2 = new Sample1707();
	thread1.start();
	thread2.start();
    }

    public void run(){
	sheet.reserve(this.getName());
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1707
Thread-1 が予約確認: シート予約可能
Thread-1 の予約処理中...
Thread-1 のシート予約が完了
Thread-2 が予約確認: シート予約可能
Thread-2 の予約処理中...
Thread-2 のシート予約が完了

最初のスレッドが予約処理中はシートオブジェクトがロックされるので, ロック解除後 (予約処理終了後) になってから別のスレッドが予約処理を行うようになり, 正しく予約管理ができている.

デッドロック

排他制御は一つのスレッドがオブジェクトの使用中にそのオブジェクトをロックすることで実現しているが, 複数のオブジェクトに対して排他制御を行うような時, その使用を間違えると思わぬ事態に陥ることがある.

例えば, A, B, 二つのオブジェクトがあり, A オブジェクトをロックしているスレッドが B オブジェクトのロックを待ちをしているとき, 万が一 B オブジェクトをロックしているスレッドが A オブジェクトのロック解除待ちになってしまっていたら, 永久に待ち合うことになってしまう.

このように二つのスレッドがお互いにオブジェクトのロック解除を待ち合って, その結果, 処理がストップしてしまうことが [デッドロック] と言う.

排他制御の実例で使用したサンプルを変更してデッドロックの検証をする. プログラムの流れは次の通りである:

1. 共通オブジェクトとしてシート (Sample1708Sheet) が二つある. 二つのスレッドがシートの予約処理 (reserve メソッド) を呼び出す.
2. シートは二つとも既に予約されており, [予約済み] と表示して, そのあと更に, もう一つのシートの予約処理を行う.

Sample1708.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Sample1708Sheet{
    private String sheetName;

    Sample1708Sheet(String name){
	sheetName = name;
    }

    public synchronized void reserve(String name){
	System.out.println(name + " が ["+sheetName+"] の予約確認: 予約済み");
    }

    public synchronized void reserve(String name, Sample1708Sheet subSheet){
	System.out.println(name + " が ["+sheetName+"] の予約確認: 予約済み");
	try{
	    Thread.sleep(1000);
	}catch(InterruptedException e){
	    e.printStackTrace();
	}

	System.out.println(name + " が [サブシート("+subSheet.getName()+")] の予約処理を開始");
	subSheet.reserve(name);
    }

    public String getName(){
	return sheetName;
    }
}

public class Sample1708 extends Thread{
    static Sample1708Sheet sheet1;
    static Sample1708Sheet sheet2;
    static int cnt = 0;

    public static void main(String[] args){
	sheet1 = new Sample1708Sheet("Sheet1");
	sheet2 = new Sample1708Sheet("Sheet2");

	Thread thread1 = new Sample1708();
	Thread thread2 = new Sample1708();

	thread1.start();
	thread2.start();
    }

    public void run(){
	if(cnt == 0){
	    cnt++;
	    sheet1.reserve(this.getName(), sheet2);
	}else{
	    sheet2.reserve(this.getName(), sheet1);
	}
    }
}

上記のプログラムの実行結果:

[wtopia 17]$ java Sample1708
Thread-1 が [Sheet1] の予約確認: 予約済み
Thread-2 が [Sheet2] の予約確認: 予約済み
Thread-1 が [サブシート(Sheet2)] の予約処理を開始
Thread-2 が [サブシート(Sheet1)] の予約処理を開始
  C-c C-c[wtopia 17]$ [wtopia 17]$ [wtopia 17]$ exit

お互いにシートのロック解除待ちで処理が止まってしまっている. 次のようなイメージとなる.

_images/java-thread05.gif