ここでは、スレッドの動きを少し詳しくみていきます。
前回は、複数のスレッドからアクセスされるメソッドが1つの場合でしたが、そのメソッドが複数の場合はどうなるのでしょうか?
今回は、「3.スレッドの同期について(1/2)」からのサンプルコードを元に、ガソリンスタンドへのガソリン(共有資源)補給という概念を盛り込んで説明したいと思います。
以下の仕様、サンプルコードで具体例を示します。
※なお、赤字の箇所については、「3-1.非同期処理の場合」「3-2.同期処理の場合」からの追加・変更点を表しています。
<<サンプルコードの仕様説明>> ※仕様追加・変更箇所は赤字で記述
・RacingCarクラス(スレッド処理)
車自身です。
共有のガソリンスタンド(GasStandクラス)から1周毎に1Lのガソリンを補給します。
例では、サーキット5周を走行します。
・CarRaceクラス
車(RacingCarクラス)の初期設定、及びスタートを行います。
ガソリン補給(Replenishmentクラス)の初期設定、及び補給開始を行います。
ガソリンスタンド(GasStandクラス)の初期設定(ガソリンの初期量)、残量の表示を行います。
※最後の残量表示は、全スレッドの終了(joinメソッド)を待って表示します。
例では、車を3体用意し、各々が別スレッドで処理を行い、順位を競わせます。
・GasStandクラス
ガソリンを、1L単位で車に供給します。
ガソリンを補給してもらいます。
・Replenishmentクラス(スレッド処理)※新規追加
ガソリンスタンド(GasStandクラス)にガソリン補給を行います。
CarRaceクラスの仕様より15回(1回に1L)補給します。
■サンプルコード
4-1-1. RacingCar.java
「3-1-1. RacingCar.java」と同じ
4-1-2. CarRace.java ※赤字の箇所は、「3-1-2.CarRace.java」からの追加・修正点
class CarRace{
public static void main(String args[]){
//共有使用するガソリンスタンドクラスのインスタンスを生成する
//ガソリンの初期量は0Lとする(使用した分だけ補充する)
//※1周で1L補給するとする
GasStand gasStand = new GasStand(0);
//Threadクラス継承のサブクラスのインスタンスを生成する
RacingCar racingCar1 = new RacingCar(1, gasStand);
RacingCar racingCar2 = new RacingCar(2, gasStand);
RacingCar racingCar3 = new RacingCar(3, gasStand);
//ガソリンの補給回数は、3台×5周なので15回
//※1回で1L補給
Replenishment replenishment = new Replenishment(15, gasStand);
//ガソリンスタンドのガソリン初期残量を表示する
gasStand.printTotalGas();
//生成したスレッドのインスタンスのstartメソッドを呼び出す
//(スレッド起動)
racingCar1.start();
racingCar2.start();
racingCar3.start();
replenishment.start();
//全ての車がGOALするのを待つ(全スレッド終了まで待機)
try{
racingCar1.join();
racingCar2.join();
racingCar3.join();
replenishment.join();
}catch(InterruptedException e){
e.printStackTrace();
}
//ガソリンスタンドのガソリンレース後残量を表示する
gasStand.printTotalGas();
}
}
4-1-3. GasStand.java ※赤字の箇所は、「3-2-3.GetStand.java」からの追加・修正点
class GasStand{
private int totalGas;
public GasStand(int gas){
this.totalGas = gas;
}
public synchronized void getGas(){
int tmpGas = this.totalGas;
try{
//補給時間0.5秒スリープ
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
this.totalGas = tmpGas - 1;
}
public void putGas(){
int tmpGas = this.totalGas;
try{
//補給時間0.25秒スリープ
Thread.sleep(250);
}catch(InterruptedException e){
e.printStackTrace();
}
this.totalGas = tmpGas + 1;
}
public void printTotalGas(){
System.out.println("\n■ガソリンスタンド■ ガス総量"+this.totalGas+"L\n");
}
}
4-1-4. Replenishment.java
class Replenishment extends Thread{ private GasStand gasStand = null; private int count = 0; public Replenishment(int count, GasStand gasStand){ this.count = count; //引数の値(ガソリンスタンド)をインスタンス変数に格納 this.gasStand = gasStand; } public void run(){ for (int i = 1; i <= count; i++){ //ガソリン1L補充 this.gasStand.putGas(); System.out.println("***** 補給 第"+i+"回目1L"); } } }
■実行結果イメージ
■ガソリンスタンド■ ガス総量0L
***** 補給 第1回目1L
***** 補給 第2回目1L
3号車 1周目通過
***** 補給 第3回目1L
***** 補給 第4回目1L
3号車 2周目通過
***** 補給 第5回目1L
***** 補給 第6回目1L
1号車 1周目通過
***** 補給 第7回目1L
***** 補給 第8回目1L
2号車 1周目通過
***** 補給 第9回目1L
***** 補給 第10回目1L
3号車 3周目通過
***** 補給 第11回目1L
***** 補給 第12回目1L
1号車 2周目通過
***** 補給 第13回目1L
***** 補給 第14回目1L
2号車 2周目通過
***** 補給 第15回目1L
3号車 4周目通過
1号車 3周目通過
2号車 3周目通過
3号車 5周目通過
3号車 GOAL!!
1号車 4周目通過
2号車 4周目通過
1号車 5周目通過
1号車 GOAL!!
2号車 5周目通過
2号車 GOAL!!
■ガソリンスタンド■ ガス総量-15L ← -15Lになっている!!
今回は、ガソリンスタンドのガソリン初期量を0Lから開始させています。ガソリン補給は消費量分(15L)、新規作成インスタンス(Replenishment)にて補給されているはずなので、ガソリンの処理後残量としても0Lになるはずです。しかし、結果をみると、-15Lになっています。
またまた、おかしいーーーーーーーーーー!!!!!
■結果相違の原因
これも前回と同じ原因です。
今回は、共有資源であるインスタンス(GasStand)に新しいメソッド(putGas)を追加しました。が、そのメソッドには排他制御(同期)をおこなっていませんでした。ガソリン量を操作するのは、getGas・putGasメソッドの双方で行っているので、正しい結果を得るためには、その双方に排他制御(同期)を行わなければなりません。下図「1メソッドのみ排他を考慮する場合」で説明します。
GasStandインスタンスのGetGasメソッドへのアクセス順序を見ていくと以下の様になります。
やはり、前回と同じ様な現象が発生していました。正しく処理させるためには、共有資源インスタンス(GasStand)の共有フィールド(totalGas)を操作するメソッド全てにおいて、排他制御(同期)を行わないといけないということがわかりました。(中途半端に排他制御しても意味がない)
このことを踏まえ修正したサンプルコードと結果を以下「3-2.同期処理の場合」に示します。※全メソッドの排他を考慮する場合の図・処理順の説明については割愛します。
1 メソッドのみ排他を考慮する場合
■サンプルコード
4-2-1. RacingCar
「4-1-1. RacingCar.java」と同じ
4-2-2. CarRace.java
「4-1-2. CarRace.java」と同じ
4-2-3. GasStand.java ※赤字の箇所は、「4-1-3.GasStand.java」からの追加・修正点
class GasStand{
private int totalGas;
public GasStand(int gas){
this.totalGas = gas;
}
public synchronized void getGas(){
int tmpGas = this.totalGas;
try{
//補給時間0.5秒スリープ
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
this.totalGas = tmpGas - 1;
}
public synchronized void putGas(){
int tmpGas = this.totalGas;
try{
//補給時間0.25秒スリープ
Thread.sleep(250);
}catch(InterruptedException e){
e.printStackTrace();
}
this.totalGas = tmpGas + 1;
}
public void printTotalGas(){
System.out.println("\n■ガソリンスタンド■ ガス総量"+this.totalGas+"L\n");
}
}
4-2-4. Replenishment.java
「4-1-4. Replenishment.java」と同じ
■実行結果イメージ
■ガソリンスタンド■ ガス総量0L
***** 補給 第1回目1L
3号車 1周目通過
***** 補給 第2回目1L
1号車 1周目通過
2号車 1周目通過
3号車 2周目通過
***** 補給 第3回目1L
1号車 2周目通過
***** 補給 第4回目1L
2号車 2周目通過
3号車 3周目通過
***** 補給 第5回目1L
1号車 3周目通過
***** 補給 第6回目1L
2号車 3周目通過
3号車 4周目通過
***** 補給 第7回目1L
1号車 4周目通過
2号車 4周目通過
***** 補給 第8回目1L
3号車 5周目通過
3号車 GOAL!!
1号車 5周目通過
1号車 GOAL!!
***** 補給 第9回目1L
2号車 5周目通過
2号車 GOAL!!
***** 補給 第10回目1L
***** 補給 第11回目1L
***** 補給 第12回目1L
***** 補給 第13回目1L
***** 補給 第14回目1L
***** 補給 第15回目1L
■ガソリンスタンド■ ガス総量0L ← 0Lになっている。
きちんと補給したガソリンの量だけ消費され、結果残量が0Lになっています。
■synchronizedブロックについて
上記の説明では、synchronized修飾子の使用例を説明してきました。
ここで、synchronizedブロックを使用した場合の例について、以下に2例示したいと思います。
機能的(処理結果)には、上記で示したsychronized修飾子と同じになります。
GasStand.java
class GasStand{
******************** 省略 ********************
public void getGas(){
synchronized(this){
int tmpGas = this.totalGas;
try{
//補給時間0.5秒スリープ
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
this.totalGas = tmpGas - 1;
}
}
public void putGas(){
synchronized(this){
int tmpGas = this.totalGas;
try{
//補給時間0.25秒スリープ
Thread.sleep(250);
}catch(InterruptedException e){
e.printStackTrace();
}
this.totalGas = tmpGas + 1;
}
}
******************** 省略 ********************
}
Replenishment.java
class Replenishment extends Thread{
******************** 省略 ********************
public void run(){
for (int i = 1; i <= count; i++){
synchronized(this.gasStand){
//ガソリン1L補充
this.gasStand.putGas();
}
System.out.println("***** 補給 第"+i+"回目1L");
}
}
}
RacingCar.java
class RacingCar extends Thread{
******************** 省略 ********************
public void run(){
for (int i = 1; i <= round; i++){
try{
//ランダムミリ秒数スリープにて差を出す
Thread.sleep((long)(Math.random() * 1000));
//割込み例外処理
}catch(InterruptedException e){
e.printStackTrace();
}
synchronized(this.gasStand){
//ガソリン1L補給
this.gasStand.getGas();
}
System.out.println(printOffset[number-1] + this.number+"号車 "+i+"周目通過");
}
System.out.println(printOffset[number-1] + this.number+"号車 GOAL!!");
}
}
※ここでは、インスタンスメソッドの排他制御について説明しましたが、仮にこのメソッド(getGas)がクラスメソッドだった場合は、このクラスのクラスオブジェクトであるGasStand.classを指定します。
そうなると当然ロックの対象となるオブジェクトも異なってくるので注意が必要です。
synchronizedブロックの使用例を示しましたが、2のやり方については、あまり実用的ではないように思います。
なぜかというと、共有資源インスタンスのメソッドを呼出す箇所全てにおいてsynchronizedブロック制御を行わなければならないからです。(その制御をどこか1箇所でも忘れると制御自体の意味がなくなりますし・・・)
なので2については、こういう風にも使えるんだな程度に見ていただければこれ幸いです。
また、synchronized修飾子の説明でメソッドに排他をかける様な言い回しをしましたが、注意してください!!
synchronizedブロックの()でインスタンスを指定していることからもわかる様に、排他の範囲は、「メソッド」ではなく「インスタンス」です。「メソッドを排他的に実行するための宣言」ではありません!!
異なるインスタンスであれば、待たされずに同時に実行されますし、今回の様にsynchronizedを付けた別のメソッドであっても、同じインスタンスに対するものならば同時に実行されません。