本節では、スワップ(swap)と呼ばれる値の交換によってマルチスレッドの理解を進めていきます。
例えば、int型配列 aの a[2]と a[5]の内容を入れ替えようとすると、以下のようなソースコードになります。
int temp = a[2]; a[2] = a[5]; a[5] = temp;
変数 tempを使用することによって、値を変更することができます。
シングルスレッドで値の交換をするプログラムを挙げます。キーボードから指定した回数だけ、値の交換を実施します。その後、値を表示し、データの破損がないかどうかをチェックします。
ソースコードは以下の通り。
Q301/MySystem.java
(ライブラリをそのまま利用します)
Q301/Q301.java
/** * シングルスレッドで値の交換を行います。 */ public class Q301 { /** * 交換回数。 */ private int swapCount; /** * 数群。 */ private int[] numbers = new int[25]; /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Q301 q301 = new Q301(); q301.execute(); } /** * 実行します。 */ public void execute() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); // 初期値の設定 for(int i = 0; i < this.numbers.length; i ++) { this.numbers[i] = i + 1; } this.printNumbers("交換前"); this.swapCount = MySystem.in.getInt("スレッドごとの交換回数は"); this.swapNumbers(this.swapCount); this.printNumbers(this.swapCount + "回交換後"); this.checkNumbers(); } /** * 数群を表示します。 * @param message メッセージ */ private void printNumbers(String message) { System.out.print(message + ": "); for(int i = 0; i < this.numbers.length; i ++) { System.out.print(this.numbers[i]); if(i != this.numbers.length - 1) { System.out.print(","); } } System.out.println(); } /** * 指定された回数、値を交換します。 * @param times 回数 */ private void swapNumbers(int times) { for(int i = 0; i < times; i ++) { this.swap(); } } /** * 値を交換します。 */ private void swap() { int value1 = (int)(Math.random() * this.numbers.length); int value2 = (int)(Math.random() * this.numbers.length); int temp = this.numbers[value1]; this.numbers[value1] = this.numbers[value2]; this.numbers[value2] = temp; } /** * 数群の値をチェックします。 */ private void checkNumbers() { boolean isAllOK = true; for(int j = 0; j < this.numbers.length; j ++) { boolean isOK = false; for(int i = 0; i < this.numbers.length; i ++) { if(this.numbers[i] == j + 1) { isOK = true; } } if(! isOK) { System.out.println((j + 1) + "が見つかりません。"); isAllOK = false; } } if(isAllOK) { System.out.println("異常はありませんでした。"); } } }
まず、1回だけ値を交換してみます。実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1 1回交換後: 1,2,3,4,5,6,7,8,9,10,23,12,13,14,15,16,17,18,19,20,21,22,11,24,25 異常はありませんでした。
23と 11が入れ替わっていることが分かります。
続いて、1000回、100万回を指定して値の交換を実行してみます。
実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1000 1000回交換後: 6,2,24,21,4,11,12,1,5,3,9,7,15,19,23,18,16,20,13,22,10,25,14,17,8 異常はありませんでした。
実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1000000 1000000回交換後: 1,14,3,25,20,5,22,18,23,17,10,9,11,8,4,24,21,2,6,16,13,7,15,19,12 異常はありませんでした。
いずれの場合も「異常はありませんでした。」と表示されています。値の交換は上手くできていることが分かります。
スワップのプログラムを 2つのスレッドによって実行させたらどうなるのかを実験してみます。
特段の実用性がある訳ではなく、ただの実験としてマルチスレッドを扱ってみます。
2つのスレッドによって、スワップを行うプログラムを作成してみました。
ソースコードは以下の通り。
Q302/MySystem.java
(ライブラリをそのまま利用します)
Q302/Q302.java
/** * マルチスレッドで値の交換を行います。失敗例。 */ public class Q302 implements Runnable { /** * 交換回数。 */ private int swapCount; /** * 数群。 */ private int[] numbers = new int[25]; /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Q302 q301 = new Q302(); q301.execute(); } /** * 実行します。 */ public void execute() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); // 初期値の設定 for(int i = 0; i < this.numbers.length; i ++) { this.numbers[i] = i + 1; } this.printNumbers("交換前"); this.swapCount = MySystem.in.getInt("スレッドごとの交換回数は"); // 別スレッドを起動します。 Thread thread = new Thread(this); thread.start(); this.swapNumbers(this.swapCount); try { Thread.sleep(2000); } catch (InterruptedException e) { ; // 何もしない } this.printNumbers(this.swapCount + "回交換後"); this.checkNumbers(); } @Override public void run() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); this.swapNumbers(this.swapCount); System.out.println(Thread.currentThread().getName() + "は終了。"); } /** * 数群を表示します。 * @param message メッセージ */ private void printNumbers(String message) { System.out.print(message + ": "); for(int i = 0; i < this.numbers.length; i ++) { System.out.print(this.numbers[i]); if(i != this.numbers.length - 1) { System.out.print(","); } } System.out.println(); } /** * 指定された回数、値を交換します。 * @param times 回数 */ private void swapNumbers(int times) { for(int i = 0; i < times; i ++) { this.swap(); } } /** * 値を交換します。 */ private void swap() { int value1 = (int)(Math.random() * this.numbers.length); int value2 = (int)(Math.random() * this.numbers.length); int temp = this.numbers[value1]; this.numbers[value1] = this.numbers[value2]; this.numbers[value2] = temp; } /** * 数群の値をチェックします。 */ private void checkNumbers() { boolean isAllOK = true; for(int j = 0; j < this.numbers.length; j ++) { boolean isOK = false; for(int i = 0; i < this.numbers.length; i ++) { if(this.numbers[i] == j + 1) { isOK = true; } } if(! isOK) { System.out.println((j + 1) + "が見つかりません。"); isAllOK = false; } } if(isAllOK) { System.out.println("異常はありませんでした。"); } } }
まず、1回だけ値を交換してみます。実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1 現在のスレッドはThread-0です。 Thread-0は終了。 1回交換後: 1,2,3,4,5,6,13,8,9,10,11,12,7,14,25,16,17,18,19,20,21,22,23,24,15 異常はありませんでした。
スレッド当たり 1回なので、合計 2箇所の値が交換されます。この例では、13と 7、25と 15が交換されていることが分かります。
続いて、もっと大きな数と言うことで 1000回を指定してみます。
実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1000 現在のスレッドはThread-0です。 Thread-0は終了。 1000回交換後: 1,16,5,12,22,3,19,21,4,24,8,9,11,13,10,20,18,17,23,2,6,15,25,14,7 異常はありませんでした。
「異常はありませんでした」と表示されています。問題ないのでしょうか。
さらに大きな数、100万回を指定してみました。実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1000000 現在のスレッドはThread-0です。 Thread-0は終了。 1000000回交換後: 1,21,1,21,21,1,17,1,21,17,21,21,17,21,1,1,21,1,21,21,1,1,17,21,21 2が見つかりません。 3が見つかりません。 4が見つかりません。 5が見つかりません。 6が見つかりません。 7が見つかりません。 8が見つかりません。 9が見つかりません。 10が見つかりません。 11が見つかりません。 12が見つかりません。 13が見つかりません。 14が見つかりません。 15が見つかりません。 16が見つかりません。 18が見つかりません。 19が見つかりません。 20が見つかりません。 22が見つかりません。 23が見つかりません。 24が見つかりません。 25が見つかりません。
データが破損してしまいました。
バグが発生した原因は、配列 numbersを 2つのスレッドが同時にアクセスした場合に、ふたつのスレッドのタイミングによって、値の交換が上手く行われない可能性があるためです。
原因を詳しく説明しようとするととても手間がかかるためざっくり説明すると、swapメソッドにおいて値を交換する際に、例えば、メインメソッドが numbers[3]と numbers[5]を交換しようとし、それと同じタイミングで、別スレッドが numbers[5]と numbers[3]を交換しようとした場合に、値がおかしなことになってしまいます。
今回の例において、僕の環境では 1000回を指定した場合には、ほとんどバグが発生しませんでした。逆に言えば、バグの発生頻度は「1000回に 1回未満」ということでもあります。ということは、実際にバグが発生しても、バグを再現することは極めて困難ということになります。という訳で、マルチスレッドにバグを潜めてしまった場合には、デバッグ作業が極めて困難になります。
スレッド間で同期をとるための方法は幾つかありますが、synchronizedブロックは、そのうちのひとつです。読み方はシンクロナイズド、一般には「シンクロナイズドスイミング」という言葉でも使われています。「同期をとる」という意味です。
ロックするインスタンスを指定することで、指定した区間について複数のスレッドが同時に処理をすることを防ぐことができます。
以下のソースコードを考えて見ます。
synchronized(object) { ; // 処理1 ; // 処理2 }
あるスレッドがこの部分を処理しようとした場合、object変数の参照先のインスタンスをロックして、この処理の中に入ります。このとき、別のスレッドが、この部分や他の synchronizedブロックを処理しようとしたとき、もしロック対象がそのインスタンスであったならば、ロックが解除されるまで、処理を待ちます。
これによって、スレッド同士の同期をとることができます。
以下の 3つの条件を満たしたとき、synchronizedブロックを synchronizedなインスタンスメソッドに置き換えできます。
具体的には以下のような場合です。
public void method() { synchronized(this) { ; // 処理1 ; // 処理2 } }
このような場合、以下のように synchronizedなインスタンスメソッドに置き換えできます。
public synchronized void method() { ; // 処理1 ; // 処理2 }
ロックするオブジェクトが分かりづらくなりますが、自分自身(this)であることをに留意してください。
synchronizedな staticなメソッドも文法上は可能です。
public static synchronized void method() { ; // 処理1 ; // 処理2 }
この場合、ロックするオブジェクトが何なのかが問題になります。本ウェブサイトでは触れないこととします。
スレッド間で同期をとるために synchronizedブロックを使用します。今回のプログラムでは swapがインスタンスメソッドということもあり、自分自身(this)をロック対象とするようにプログラムを記述しました。
ソースコードは以下の通り。
Q303/MySystem.java
(ライブラリをそのまま利用します)
Q303/Q303.java
/** * マルチスレッドで値の交換を行います。 */ public class Q303 implements Runnable { /** * 交換回数。 */ private int swapCount; /** * 数群。 */ private int[] numbers = new int[25]; /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Q303 q301 = new Q303(); q301.execute(); } /** * 実行します。 */ public void execute() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); // 初期値の設定 for(int i = 0; i < this.numbers.length; i ++) { this.numbers[i] = i + 1; } this.printNumbers("交換前"); this.swapCount = MySystem.in.getInt("スレッドごとの交換回数は"); // 別スレッドを起動します。 Thread thread = new Thread(this); thread.start(); this.swapNumbers(this.swapCount); try { Thread.sleep(2000); } catch (InterruptedException e) { ; // 何もしない } this.printNumbers(this.swapCount + "回交換後"); this.checkNumbers(); } @Override public void run() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); this.swapNumbers(this.swapCount); System.out.println(Thread.currentThread().getName() + "は終了。"); } /** * 数群を表示します。 * @param message メッセージ */ private void printNumbers(String message) { System.out.print(message + ": "); for(int i = 0; i < this.numbers.length; i ++) { System.out.print(this.numbers[i]); if(i != this.numbers.length - 1) { System.out.print(","); } } System.out.println(); } /** * 指定された回数、値を交換します。 * @param times 回数 */ private void swapNumbers(int times) { for(int i = 0; i < times; i ++) { this.swap(); } } /** * 値を交換します。 */ private void swap() { synchronized(this) { int value1 = (int)(Math.random() * this.numbers.length); int value2 = (int)(Math.random() * this.numbers.length); int temp = this.numbers[value1]; this.numbers[value1] = this.numbers[value2]; this.numbers[value2] = temp; } } /** * 数群の値をチェックします。 */ private void checkNumbers() { boolean isAllOK = true; for(int j = 0; j < this.numbers.length; j ++) { boolean isOK = false; for(int i = 0; i < this.numbers.length; i ++) { if(this.numbers[i] == j + 1) { isOK = true; } } if(! isOK) { System.out.println((j + 1) + "が見つかりません。"); isAllOK = false; } } if(isAllOK) { System.out.println("異常はありませんでした。"); } } }
100万回を指定したときの実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1000000 現在のスレッドはThread-0です。 Thread-0は終了。 1000000回交換後: 22,1,24,20,5,11,14,23,18,17,7,15,21,12,16,3,13,25,19,4,6,9,10,2,8 異常はありませんでした。
正常に値交換ができています。
なお、今回のプログラムの場合、自分自身(this)ではなく変数 numbersの参照先インスタンスをロック対象にしたとしても、上手くスレッド間で同期をとることができます。
Q303プロジェクトでは、メソッドはインスタンスメソッドで、その全体を synchronizedブロックで覆い、かつロック対象が自分自身(this)だったので、synchronizedなインスタンスメソッドに置き換えることができます。その例を挙げます。
ソースコードは以下の通り。
Q304/MySystem.java
(ライブラリをそのまま利用します)
Q304/Q304.java
/** * マルチスレッドで値の交換を行います。 */ public class Q304 implements Runnable { /** * 交換回数。 */ private int swapCount; /** * 数群。 */ private int[] numbers = new int[25]; /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Q304 q301 = new Q304(); q301.execute(); } /** * 実行します。 */ public void execute() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); // 初期値の設定 for(int i = 0; i < this.numbers.length; i ++) { this.numbers[i] = i + 1; } this.printNumbers("交換前"); this.swapCount = MySystem.in.getInt("スレッドごとの交換回数は"); // 別スレッドを起動します。 Thread thread = new Thread(this); thread.start(); this.swapNumbers(this.swapCount); try { Thread.sleep(2000); } catch (InterruptedException e) { ; // 何もしない } this.printNumbers(this.swapCount + "回交換後"); this.checkNumbers(); } @Override public void run() { System.out.println("現在のスレッドは" + Thread.currentThread().getName() + "です。"); this.swapNumbers(this.swapCount); System.out.println(Thread.currentThread().getName() + "は終了。"); } /** * 数群を表示します。 * @param message メッセージ */ private void printNumbers(String message) { System.out.print(message + ": "); for(int i = 0; i < this.numbers.length; i ++) { System.out.print(this.numbers[i]); if(i != this.numbers.length - 1) { System.out.print(","); } } System.out.println(); } /** * 指定された回数、値を交換します。 * @param times 回数 */ private void swapNumbers(int times) { for(int i = 0; i < times; i ++) { this.swap(); } } /** * 値を交換します。 */ private synchronized void swap() { int value1 = (int)(Math.random() * this.numbers.length); int value2 = (int)(Math.random() * this.numbers.length); int temp = this.numbers[value1]; this.numbers[value1] = this.numbers[value2]; this.numbers[value2] = temp; } /** * 数群の値をチェックします。 */ private void checkNumbers() { boolean isAllOK = true; for(int j = 0; j < this.numbers.length; j ++) { boolean isOK = false; for(int i = 0; i < this.numbers.length; i ++) { if(this.numbers[i] == j + 1) { isOK = true; } } if(! isOK) { System.out.println((j + 1) + "が見つかりません。"); isAllOK = false; } } if(isAllOK) { System.out.println("異常はありませんでした。"); } } }
100万回を指定したときの実行結果の例は以下の通り。
現在のスレッドはmainです。 交換前: 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 スレッドごとの交換回数は? 1000000 現在のスレッドはThread-0です。 Thread-0は終了。 1000000回交換後: 21,19,5,4,24,10,11,2,22,6,9,23,7,8,15,16,18,25,14,13,12,3,1,20,17 異常はありませんでした。
正常に値交換ができています。