スレッド動作の詳解(3/7)

ここでは、スレッドの動きを少し詳しくみていきます。

  1. スレッドについて
  2. スレッドの実装方法について
  3. スレッドの同期について(1/2)
  4. スレッドの同期について(2/2)
  5. スレッドの処理順制御について
  6. スレッドの同時実行可否について
  7. デッドロックについて

3. スレッドの同期について(1/2)

複数のスレッドが、共有資源にアクセスする場合、矛盾が発生する場合があります。その時に必要となってくるのが、排他制御(同期)という概念です。

今回は、「2. Threadの実装方法について」のサンプルコードを元に、ガソリン(共有資源)消費という概念を盛り込んで説明したいと思います。


以下の仕様、サンプルコードで具体例を示します。

※なお、赤字の箇所については、「2-1. Threadクラスを継承する場合」からの追加・変更点を表しています。


<<サンプルコード仕様説明>>  ※仕様変更・変更箇所は赤字で記述
  ・RacingCarクラス(スレッド処理)
    車自身です。
    共有のガソリンスタンド(GasStandクラス)から1周毎に1Lのガソリンを補給します。
    例では、サーキット5周を走行します。

    ・CarRaceクラス
    車(RacingCarクラス)の初期設定、及びスタートを行います。
    ガソリンスタンド(GasStandクラス)の初期設定(ガソリンの初期量)、残量の表示を行います。
    ※最後の残量表示は、全スレッドの終了(joinメソッド)を待って表示します。
    例では、車を3体用意し、各々が別スレッドで処理を行い、順位を競わせます。
    また、消費予定のガソリン量15L(3台×5周:1周当り1L消費するとする)を予め初期セットします。

    ・GasStandクラス ※新規追加
    ガソリンを、初期量(15L)から、1L単位で車に供給します。
        

3-1. 非同期処理の場合

■サンプルコード

3-1-1. RacingCar.java ※赤字の箇所は、「2-1-1.RacingCar.java」からの追加・修正点

class RacingCar extends Thread{
    private int number = 0;
    private GasStand gasStand = null;
    private static int round = 5;
    private static String[] printOffset = {"", "\t\t\t", "\t\t\t\t\t\t"};

    public RacingCar(int number, GasStand gasStand){
        //引数の値(車番)をインスタンス変数に格納
        this.number = number;
        //引数の値(ガソリンスタンド)をインスタンス変数に格納
        this.gasStand = gasStand;
    }

    public void run(){
        for (int i = 1; i <= round; i++){
            try{
                //ランダムミリ秒数スリープにて差を出す
                Thread.sleep((long)(Math.random() * 1000));
                //割込み例外処理
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            //ガソリン1L補給
            this.gasStand.getGas();
            System.out.println(printOffset[number-1] + this.number+"号車 "+i+"周目通過");
        }
        System.out.println(printOffset[number-1] + this.number+"号車 GOAL!!");
    }
}

3-1-2. CarRace.java ※赤字の箇所は、「2-1-2.CarRace.java」からの追加・修正点

class CarRace{
    public static void main(String args[]){
        //共有使用するガソリンスタンドクラスのインスタンスを生成する
        //ガソリンの量は3台×5周なので、15L用意する
        //※1周で1L補給するとする
        GasStand gasStand = new GasStand(15);

        //Threadクラス継承のサブクラスのインスタンスを生成する
        RacingCar racingCar1 = new RacingCar(1, gasStand);
        RacingCar racingCar2 = new RacingCar(2, gasStand);
        RacingCar racingCar3 = new RacingCar(3, gasStand);

        //ガソリンスタンドのガソリン初期残量を表示する
        gasStand.printTotalGas();

        //生成したスレッドのインスタンスのstartメソッドを呼び出す
        //(スレッド起動)
        racingCar1.start();
        racingCar2.start();
        racingCar3.start();

        //全ての車がGOALするのを待つ(全スレッド終了まで待機)
        try{
          racingCar1.join();
          racingCar2.join();
          racingCar3.join();
      }catch(InterruptedException e){
          e.printStackTrace();
      }

      //ガソリンスタンドのガソリンレース後残量を表示する
      gasStand.printTotalGas();
      }
}

3-1-3. GasStand.java

class GasStand{
    private int totalGas;

    public GasStand(int gas){
        this.totalGas = gas;
    }

    public void getGas(){
        int tmpGas = this.totalGas;
        try{
            //補給時間0.5秒スリープ
            Thread.sleep(500);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        this.totalGas = tmpGas - 1;
    }

    public void printTotalGas(){
        System.out.println("\n■ガソリンスタンド■ ガス総量"+this.totalGas+"L\n");
    }
}

■実行結果イメージ

■ガソリンスタンド■ ガス総量15L

1号車 1周目通過
                        2号車 1周目通過
                                                3号車 1周目通過
                        2号車 2周目通過
1号車 2周目通過
1号車 3周目通過
                                                3号車 2周目通過
                        2号車 3周目通過
1号車 4周目通過
                                                3号車 3周目通過
                        2号車 4周目通過
1号車 5周目通過
1号車 GOAL!!
                        2号車 5周目通過
                        2号車 GOAL!!
                                                3号車 4周目通過
                                                3号車 5周目通過
                                                3号車 GOAL!!

■ガソリンスタンド■ ガス総量9L ← 9Lではおかしい!!
        

今回の処理としては、ガソリンスタンドに初期ガソリン量15L保持させて、そこから、各々の車(スレッド)が、1周毎に1L(5週で5L)で、全車3台で、15L消費されるはずなので、15L - 15L = 0L のはずです。しかし、結果を見ると9Lになっています。

おかしいーーーーーーーーーー!!!!!


■結果相違の原因

これは、共有資源であるインスタンス(GasStand)のガソリン供給メソッド(getGas)へのアクセスの仕方に原因があります。(もっと言うとそのメソッド内でアクセスしているガソリン量フィールド値(totalGas)へのアクセスの仕方)下図「排他を考慮しない場合(非同期)」で説明します。


~ 非同期処理 ~

GasStandインスタンスのGetGasメソッドへのアクセス順序を見ていくと以下の様になります。

  1. 1号車スレッドがtotalGasフィールドの値10Lを取得
  2. 2号車スレッドがtotalGasフィールドの値10Lを取得
  3. 1号車スレッドがtotalGasフィールドに9L(1取得値10L - 消費量1L)をセット
  4. 2号車スレッドがtotalGasフィールドに9L(2取得値10L - 消費量1L)をセット

ここで問題となるのは、1と2で同じ値を取得し、3と4で同じ値をセットしているということです。正しく処理させるためには、1号車スレッドがgetGasメソッドにアクセス(1、3)している間は、2号車スレッドにアクセスさせないという制御が必要になります。これを実現するために、synchronizedという修飾子、またはブロックを使用します。

synchronized修飾子を使用した場合を、下図「排他を考慮する場合(同期)」で説明します。


排他を考慮しない場合(非同期)

非同期

~ 同期処理 ~

この場合で、GasStandインスタンスのGetGasメソッドへのアクセス順序を見ていくと以下の様になります。

  1. 1号車スレッドがtotalGasフィールドの値10Lを取得
  2. 2号車スレッドがgetGasメソッドにアクセスしようとするが、1号車スレッドがアクセス中(ロック)なので、アクセス終了(ロック解除)まで待機
  3. 1号車スレッドがtotalGasフィールドに9L(1取得値10L - 消費量1L)をセットし、getGasメソッドへのアクセスが終了
  4. 1号車スレッドのgetGasメソッドへのアクセスが終了したので、2号車スレッドがgetGasメソッドへアクセスでき、totalGasフィールドの値9Lを取得
  5. 2号車スレッドがtotalGasフィールドに8L(4取得値9L - 消費量1L)をセット

他スレッドの処理を意識して制御をかければ、上記の様にうまくいきます。


排他を考慮する場合(同期)

同期

複数のスレッドから共有資源(GasStandインスタンス)のメソッドへアクセスする場合でも、他スレッドの処理を意識し、制御しておくとうまくいきます。

synchronized修飾子を使用したサンプルコードと結果を以下「3-2.同期処理の場合」に示します。


3-2. 同期処理の場合

■サンプルコード

3-2-1. RacingCar.java

  「3-1-1. RacingCar.java」と同じ


3-2-2. CarRace.java

  「3-1-2. CarRace.java」と同じ


3-2-3. GasStand.java ※赤字の箇所は、「3-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 void printTotalGas(){
        System.out.println("\n■ガソリンスタンド■ ガス総量"+this.totalGas+"L\n");
    }
}

■実行結果イメージ

■ガソリンスタンド■ ガス総量15L

                        2号車 1周目通過
                                                3号車 1周目通過
1号車 1周目通過
                        2号車 2周目通過
                                                3号車 2周目通過
                        2号車 3周目通過
1号車 2周目通過
                                                3号車 3周目通過
                        2号車 4周目通過
1号車 3周目通過
                                                3号車 4周目通過
                        2号車 5周目通過
                        2号車 GOAL!!
                                                3号車 5周目通過
                                                3号車 GOAL!!
1号車 4周目通過
1号車 5周目通過
1号車 GOAL!!

■ガソリンスタンド■ ガス総量0L ← 0Lになっている。
        

きちんと15L(車3台×5周 :1L/1周)消費され、0Lになっています。

▲PageTop


  1. スレッドについて
  2. スレッドの実装方法について
  3. スレッドの同期について(1/2)
  4. スレッドの同期について(2/2)
  5. スレッドの処理順制御について
  6. スレッドの同時実行可否について
  7. デッドロックについて