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

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

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

7. デッドロックについて

7-1. デッドロック

デッドロックとは、2つのスレッドが2つのロックを取り合い、互いに相手のスレッドがロックを解放するのを待つ状態のことを言います。

具体例で説明します。

例えば、AさんとBさんがコンビニ弁当を1つ買って、一緒に仲良く食べることになったとします。

しかし、袋を開けるとお箸が1セット(左右の2本)しか入っていませんでした。

2本の内の1本(左)のお箸をAさんが手にとりました。すかさずBさんも片方(右)のお箸を手にとりました。

2人とも不器用なので2本(左右)のお箸がないと弁当を食べることができません。

Aさんは、仕方なくBさんがお箸を置くのを待ちます。

Bさんも同じくAさんがお箸を置くのを待ちます。

・・・・

結局、2人とも決して自分の持っているお箸を譲らなかったので餓死してしまいました。・・・・

めでたし!めでたし!・・・ってめでたくないわー!!


この様にAさんとBさんをスレッドとして見立てた場合、その双方のスレッドがそれ以上処理を進めることが出来なくなった状態をデッドロックといいます。

以下にデッドロックイメージ図、サンプルコードおよび仕様、および結果を示します。

※説明を単純化するために、「2つ(2人)のスレッドが」と説明しましたが、実際の処理では、複数のスレッドでデッドロックが発生する場合もあります。


■デッドロックイメージ図

デッドロックイメージ

7-2. デッドロックの検証

引き続き、デッドロックの動作を検証したいと思います。検証は、以下の仕様(上記で説明した内容を詳細化)、サンプルコードにて行いたいと思います。


<<サンプルコードの仕様説明>>
  ・ChopSticksクラス
    お箸をあらわすクラスです。
    このクラスのインスタンスはお箸2本(左右)のうちのどちらか一方をあらわします。

  ・DeadLockTestクラス
    お箸(ChopSticksクラス)の初期設定、及び食事開始の合図を行います。
    例では、お箸を左右の2本用意し、AさんBさんに食事の開始を促します。

  ・Humanクラス(スレッド処理)
    人をあらわすクラスです。
    このクラスのインスタンスとして、AさんとBさんを生成させます。

■サンプルコード


7-2-1. ChopSticks.java

class ChopSticks{
    private final String partName;

    public ChopSticks(String partName){
        this.partName = partName;
    }

    public void getStick(String humanName){
        //取得できたお箸を表示
        System.out.println(humanName+" が、["+partName+"] を取得!!");
    }
}

7-2-2. DeadLockTest.java

class DeadLockTest{
    public static void main(String args[]){
        //お箸を初期化セット
        ChopSticks leftStick  = new ChopSticks("左のお箸");
        ChopSticks rightStick = new ChopSticks("右のお箸");
        //Aさん、Bさんのスレッドインスタンスを生成
        Human humanA = new Human("Aさん", leftStick, rightStick);
        Human humanB = new Human("Bさん", rightStick, leftStick);
        //スレッドスタート(いっただきまーす)
        humanA.start();
        humanB.start();
    }
}

7-2-3. Human.java

class Human extends Thread{
    private final String name;
    private final ChopSticks stick1;
    private final ChopSticks stick2;

    public Human(String name, ChopSticks stick1, ChopSticks stick2){
        this.name = name;
        this.stick1 = stick1;
        this.stick2 = stick2;
    }

    public void run(){
        System.out.println(this.name+" が、いただきます しました!!");

        for(int i = 0; i < 10; i++){
            try{
                //コンビ二弁当を食べる
                eat();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        System.out.println(this.name+" が、ごちそうさま しました!!");

    }

    private void eat() throws InterruptedException{
        synchronized(this.stick1){
            //片方のお箸を取得
            this.stick1.getStick(this.name);
            //デッドロックが発生しやすくするためスリープを実行
            //Thread.sleep(10);
            synchronized(this.stick2){
                //もう片方のお箸を取得
                this.stick2.getStick(this.name);
                //デッドロックが発生しやすくするためスリープを実行
                //Thread.sleep(10);
            }
        }
    }
}

■実行結果イメージ

「Ctrl」キー「c」キーを同時押下すれば、終了させることができます。

Aさん が、いただきます しました!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Bさん が、いただきます しました!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
_

やはり、途中で止まってしまいました。下からの2行の出力を見る限り、以下の様な現象が発生しています。

  1. Aさんが「左のお箸」を取得できたので、「右のお箸」を取りにいこうとするが、その前にBさんが「右のお箸」を取得する
  2. 仕方がないので、AさんはBさんが「右のお箸」を置くまで待機する
  3. 一方Bさんは「右のお箸」を取得できたので、「左のお箸」を取りにいこうとするが、既にAさんが取得しているので待機する。
  4. 双方待機状態になり処理が止まる
  5. デッドロック発生!!

何度か実行してもらえれば分かると思うのですが、今回はたまたまデッドロックを検知できたにすぎません。デッドロックが発生せずに終了してしまう場合があるからです。

今回の様な単純なプログラムの場合だと、比較的簡単にデッドロックが発生していることを検知できたのですが、複雑なプログラムになるとなかなか分かりません。なのでデッドロックを解析するのは、非常に困難な作業となります。でも、デッドロックを発生しやすくし、検知しやすくする方法はあります。


上記サンプルコードの「7-2-3.Human.java」のeatメソッドの中を見てください。ここでは、コメントにしてありますが、sleepメソッドを使用しています。これは、ロックを取得した直後に処理を休止させ、デッドロックが発生しやすくするためのタイミングを計っています。

以下に、コメントをはずした場合の実行結果を示します。


■実行結果イメージ

Aさん が、いただきます しました!!
Aさん が、[左のお箸] を取得!!
Bさん が、いただきます しました!!
Bさん が、[右のお箸] を取得!!
_

初回でデッドロックが発生しました。この様に、sleepメソッドでタイミングを計ってやることにより、デッドロックを検知しやすくすることが可能です。

※必ず検知できるとは限りません


7-3. デッドロックの発生要因、及び防止方法

■デッドロックの発生要因

一般的にデッドロックの発生要因としては、以下の3点が考えられます。

1. 複数のスレッドが、複数の共有資源インスタンスにアクセスしている
今回の場合でいくと、複数の共有資源インスタンスとしては、「左のお箸」と「右のお箸」が相当します。
2. スレッドが、ある共有資源インスタンスをロックした状態で、その他の共有資源インスタンスのロックを取得しようとする
今回の場合でいくと、Aさんが「左のお箸」を取得した状態(ロック)で、もう一方の「右のお箸」を取得しようとすることに相当します。
3. 共有資源インスタンスのロックを取得する順番が決まっていない
ロックを取得する順番が対象になっている。
※ロックを取得する順番が対象とは、今回の場合でいくと、「左のお箸を取得してから右のお箸を取得する」という場合と「右のお箸を取得してから左のお箸を取得する」という場合の両方があることに相当します。

■デッドロックの防止方法

デッドロックの防止方法としては、上記で挙げた3点のうちいづれか1点を解消すればOKです。具体的にどう対応するかについては、以下に示します。

1の場合
この場合については、共有資源インスタンスを1つにすれば良いのですが、「左のお箸」「右のお箸」は別々という仕様を変えることになるので、割愛します。
2の場合
この場合については、個々で制御するのをやめ、セットで制御する方法にすることでデッドロックを解消します。つまり、左のお箸を取得、右のお箸を取得とするのではなく、左右のお箸をセットで取得する方法を取れば良いのです。

7-3-1 ChopSticks.java

  「7-2-1. ChopSticks.java」と同じ


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

class DeadLockTest{
    public static void main(String args[]){
        //お箸を初期化セット
        ChopSticks leftStick  = new ChopSticks("左のお箸");
        ChopSticks rightStick = new ChopSticks("右のお箸");
        //左右のお箸をセットにします
        StickSet stickSet = new StickSet(leftStick, rightStick);
        //Aさん、Bさんのスレッドインスタンスを生成
        Human humanA = new Human("Aさん", stickSet);
        Human humanB = new Human("Bさん", stickSet);
        //スレッドスタート(いっただきまーす)
        humanA.start();
        humanB.start();
    }
}

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

class Human extends Thread{
    private final String name;
    private final StickSet stickSet;

    public Human(String name, StickSet stickSet){
        this.name = name;
        this.stickSet = stickSet;
    }

    public void run(){
        System.out.println(this.name+" が、いただきます しました!!");

        for(int i = 0; i < 10; i++){
            try{
                //コンビ二弁当を食べる
                eat();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        System.out.println(this.name+" が、ごちそうさま しました!!");
    }

    private void eat() throws InterruptedException{
        synchronized(this.stickSet){
            //お箸のセットを取得
            this.stickSet.getStickSet(this.name);
        }
    }
}

7-3-4. StickSet.java

class StickSet{
    private final ChopSticks leftStick;
    private final ChopSticks rightStick;

    public StickSet(ChopSticks leftStick, ChopSticks rightStick){
        //お箸をセットで初期設定
        this.leftStick = leftStick;
       this.rightStick = rightStick;
    }

    public void getStickSet(String humanName){
        //取得したお箸(左)を表示
        this.leftStick.getStick(humanName);
        //取得したお箸(右)を表示
        this.rightStick.getStick(humanName);
    }
}

■実行結果イメージ

Aさん が、いただきます しました!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Bさん が、いただきます しました!!
Aさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、[左のお箸] を取得!!
Aさん が、[右のお箸] を取得!!
Aさん が、ごちそうさま しました!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、[左のお箸] を取得!!
Bさん が、[右のお箸] を取得!!
Bさん が、ごちそうさま しました!!
        

デッドロックは発生せず、最後まで処理を進めること(ごちそうさま!!)ができました。


3の場合
この場合については、簡単です。AさんとBさんが同じ順番でお箸を取得するようにするだけでOKです。

7-3-5. ChopSticks.java

  「7-2-1. ChopSticks.java」と同じ


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

class DeadLockTest{
    public static void main(String args[]){
        //お箸を初期化セット
        ChopSticks leftStick  = new ChopSticks("左のお箸");
        ChopSticks rightStick = new ChopSticks("右のお箸");
        //Aさん、Bさんのスレッドインスタンスを生成
        Human humanA = new Human("Aさん", leftStick, rightStick);
        Human humanB = new Human("Bさん", leftStick, rightStick);
        //スレッドスタート(いっただきまーす)
        humanA.start();
        humanB.start();
    }
}

7-3-7. 

  「7-2-3. Human.java」と同じ


■実行結果イメージ

上記の場合と同じく、デッドロックは発生しなくなりました。

実行結果イメージについては、上記と同じなので割愛します。

▲PageTop


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