スレッドとは1つの処理の流れを示す単位です。ちなみに「thread」を翻訳すると「糸や筋」といった意味になります。「1つの話の流れ」と言う意味で掲示板などで話題のことをスレッドと読んだりしています。
javaは複数のスレッドを同時実行させて並列処理(マルチスレッド)を可能にしています。
よく似た処理単位として「プロセス(並列処理をさせることで「マルチプロセス」)」がありますが、プロセスはOSにインストールされたソフトウェア(プログラム)の実行単位であり、スレッドはそのプログラム中の一連の処理のような感じになります。その他、プロセスは固有のメモリ空間を持ちますが、スレッドはメモリ空間を共有するという違いがあります。したがって「マルチスレッド」は「マルチプロセス」に比べ並列処理を制御する負荷は軽くなりますが、メモリ内の変数などに対して排他制御等の考慮が必要になってきます。
スレッドは下図の状態を経て処理が遂行されていきます。
CPU資源を使用して処理を行っている状態です。次の3つの場合により遷移先が異なります。
それでは実際にスレッドの実装をおこないます。実装方法は次の2種類あります。
「java.lang.Thread 」クラスを継承して、スレッド処理を実装するには次のような書式になります。
class クラス名 extends Thread { public void run() { // スレッドの処理を記述します。 } }
上記runメソッド(スレッドの処理)を呼び出す方法は次の通りです。
Thread thread = new 上記クラス();
thread.start();
・サンプルソース(Sample1701.java)
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(); // スレッドの起動。 } }
・実行結果
C:\dev\java>javac Sample1701.java [Enter] C:\dev\java>java Sample1701 [Enter] スレッド処理でーす。
「java.lang.Runnable 」インターフェースを実装して、スレッド処理を実装するには次のような書式になります。
class クラス名 implements Runnable { public void run() { // スレッドの処理を記述します。 } }
上記runメソッド(スレッドの処理)を呼び出す方法は次の通りです。
Runnable runnable = new 上記クラス(); Thread thread = new Thread(runnable); thread.start();
・サンプルソース(Sample1702.java)
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); // 「Runable」を実装したクラスのインスタンスを引数に指定します。 thread.start(); // スレッドの起動。 } }
・実行結果
C:\dev\java>javac Sample1702.java [Enter] C:\dev\java>java Sample1702 [Enter] スレッド処理でーす。
複数のスレッドを同時処理させることを「マルチスレッド」といい、これによりスループット(処理結果の応答速度)の向上が期待できます。
但し、マルチスレッド処理を行えば、すべての事象において必ずその成果がみられる訳ではありません。まずマルチスレッド処理の仕組みを考え、どのような時にマルチスレッド処理が有効なのかを知る必要があります。
マルチスレッド処理は各スレッド(=各処理)に対してCPUの資源をタイムスライス(時分割)という技術を使って分け合うことで実現しています。例えば、「処理A」、「処理B」、「処理C」と処理していく場合、次のようなイメージになります。
シングルスレッドでは「処理A」、「処理B」、「処理C」と順に処理しているのに対して、マルチスレッドでは処理を細かく分割して、先の処理が終了するのを待たずに次々処理を行っています。これによりあたかも並列処理をしているかのようにみえる訳です。
しかしこのままのイメージだと、複数のスレッドがCPUを時分割で使用したところで、各処理の終了する順番が変わることがあってもスループット(応答速度)は変わりません。むしろ処理分割する分遅くなります!
それではどのようなときにマルチスレッドが有効なのでしょうか。それは次のように各処理の負荷の差が大きい場合です。
上記の場合、シングルスレッドでは「処理A」が終了するまで「処理B」、「処理C」は処理されません、つまりシステムからの応答が返ってきません。したがって負荷の高い処理が行われるといちいち待たされるような感じになります。
これに対してマルチスレッドの場合、負荷の大きい処理が先に起動しても、その終了するのを待つことなく次々と軽い処理が先に終了して結果を返すことができる為、スループットが向上します!
以上の結果からマルチスレッド処理とは、高負荷の処理が入っても応答をとめることなく、次の処理を進めたい場合に有効な技術といえます。
マルチスレッド処理では各スレッドは独立で動作していますので、各処理が終了する順番は保障されていません。次のプログラムを使って実際の動作を確認します。
・サンプルソース(Sample1703.java)
public class Sample1703 extends Thread { public static void main(String[] args) { System.out.println("親スレッドが開始しました。"); // 親スレッド開始。 Thread thread1 = new Sample1703(); Thread thread2 = new Sample1703(); thread1.start(); // 子スレッド1を起動。 thread2.start(); // 子スレッド2を起動。 System.out.println("親スレッドが終了しました。"); // 親スレッド終了。 } public void run() { System.out.println("子スレッド : " + this.getName() + " が開始しました。"); System.out.println("子スレッド : " + this.getName() + " が終了しました。"); } }
実行結果
C:\dev\java>javac Sample1703.java [Enter] C:\dev\java>java Sample1703 [Enter] 親スレッドが開始しました。 親スレッドが終了しました。 ← 各子スレッドが終了する前に親スレッドは既に処理を終了している。 子スレッド : Thread-0 が開始しました。 子スレッド : Thread-0 が終了しました。 子スレッド : Thread-1 が開始しました。 子スレッド : Thread-1 が終了しました。
実行結果から、実際に処理を記述した順番と各スレッド処理の終了する順番が同じではないことが解ります。
しかし処理によってはスレッドの結果を参照するなど、同期をとる必要がある場面もあるかと思います。そのような場合には「java.lang.Thread」クラスのインスタンスメソッド「join()」を使用します。
これにより呼び出しもとのスレッドは呼び出したスレッドが終了するまで待機するようになります。書式は次の通りです。
public final void join() throws InterruptedException
上記の例で、各子スレッドが終了してから親スレッドが終了するように変更すると次のようになります。
・サンプルソース(Sample1704.java)
public class Sample1704 extends Thread { public static void main(String[] args) { System.out.println("親スレッドが開始しました。"); Thread thread1 = new Sample1703(); Thread thread2 = new Sample1703(); thread1.start(); thread2.start(); try { thread1.join(); // thread1の終了を待つ。 thread2.join(); // thread2の終了を待つ。 System.out.println("親スレッドが終了しました。"); } catch (InterruptedException e) { e.printStackTrace(); } } public void run() { System.out.println("子スレッド : " + this.getName() + " が開始しました。"); System.out.println("子スレッド : " + this.getName() + " が終了しました。"); } }
実行結果
C:\dev\java>javac Sample1704.java [Enter] C:\dev\java>java Sample1704 [Enter] 親スレッドが開始しました。 子スレッド : Thread-0 が開始しました。 子スレッド : Thread-0 が終了しました。 子スレッド : Thread-1 が開始しました。 子スレッド : Thread-1 が終了しました。 親スレッドが終了しました。 ← 各子スレッドが終了した後、親スレッドが終了している。
マルチスレッド処理中、各スレッドが共通のオブジェクトを使用する場合などで思わぬ事態(データ破壊)が起こることがあります。
実際の例として、座席予約システムを行うサンプルプログラムで検証します。処理の流れは次の通りです。
・サンプルソース(Sample1705.java)
class Sample1705Sheet { private boolean vacant = true; public 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 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.reserve(this.getName()); } }
・実行結果
C:\dev\java>javac Sample1705.java [Enter] C:\dev\java>java Sample1705 [Enter] Thread-0 が予約確認 : シート予約可能です。 Thread-1 が予約確認 : シート予約可能です。 Thread-0 の予約処理中・・・ Thread-1 の予約処理中・・・ Thread-0 のシート予約が完了しました。 Thread-1 のシート予約が完了しました。
実行結果を見ると1つのシートに対して2つのスレッドが共に予約処理を完了させています。つまり最初のスレッドの予約が破棄されてしまっています。(データ破壊発生)
このれは最初のスレッドが予約処理中(シートの内部変数を予約済みに変更する前)に別のスレッドが予約処理を行ってしまったのが原因です。次のようなイメージになります。
従ってこの場合、正しく処理を行うためには1つのスレッドが予約処理(reservメソッド)中のときは、他のスレッドは予約処理が出来ないように排他制御をする必要があります。
排他制御は「synchronizedブロック」又は「synchronizedメソッド」にて行います。これにより1つのインスタンスに対して処理出来るのは1つのスレッドであることが保証されます。それぞれの書式は次の通りです。
・synchronizedブロック
synchronized { 排他制御を行う処理 }
・synchronizedメソッド
[アクセス修飾子] synchronized メソッドの戻り値 排他制御させるメソッド() {}
それでは上記サンプル(Sample1705.java)の予約処理に対して排他制御を行います。
「synchronizedブロック」を使用した場合と「synchronizedメソッド」を使用した場合、それぞれ次のようになります。
・「synchronizedブロック」での排他処理コード サンプルソース(Sample1706.java)
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()); } }
・実行結果
C:\dev\java>javac Sample1706.java [Enter] C:\dev\java>java Sample1706 [Enter] Thread-0 が予約確認 : シート予約可能です。 Thread-0 の予約処理中・・・ Thread-0 のシート予約が完了しました。 Thread-1 が予約確認 : 予約済みです。
・「synchronizedメソッド」での排他処理コード サンプルソース(Sample1706.java)
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());
}
}
・実行結果
C:\dev\java>javac Sample1707.java [Enter] C:\dev\java>java Sample1707 [Enter] Thread-0 が予約確認 : シート予約可能です。 Thread-0 の予約処理中・・・ Thread-0 のシート予約が完了しました。 Thread-1 が予約確認 : 予約済みです。
最初のスレッドが予約処理中はシートオブジェクトがロックされますので、ロック解除後(予約処理終了後)になってから別スレッドが予約処理を行うようになり、正しく予約管理ができています。
排他制御は1つのスレッドがオブジェクトの使用中にそのオブジェクトをロックすることで実現していますが、複数のオブジェクトに対して排他制御を行うような時、その使用を間違えると思わぬ事態に陥ることがあります。例えば、A、B、2つのオブジェクトがあり、AオブジェクトをロックしているスレッドがBオブジェクトのロック待ちをしているとき、万が一BオブジェクトをロックしているスレッドがAオブジェクトのロック解除待ちになってしまっていたら、永久に待ちあうことになってしまいます。
このように2つのスレッドがお互いにオブジェクトのロック解除を待ちあって、その結果、処理がストップしてしまうことをデッドロックといいます。
排他制御の実例で使用したサンプルを変更してデッドロックの検証をします。プログラムの流れは次の通りです。
・サンプルソース(Sample1708.java)
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); } } }
・実行結果
C:\dev\java>javac Sample1708.java [Enter] C:\dev\java>java Sample1708 [Enter] Thread-0 が「Sheet1」の予約確認 : 予約済みです。 Thread-1 が「Sheet2」の予約確認 : 予約済みです。 Thread-0 が「サブシート(Sheet2)」の予約処理を開始します。 Thread-1 が「サブシート(Sheet1)」の予約処理を開始します。
※「Ctrl + c」コマンドで強制終了させてください。(Windowsの場合)
お互いがシートのロック解除待ちで処理が止まってしまっています。次のようなイメージです。