確実にデッドロックに陥るプログラムを書く[クローズ]


86

私は最近、この質問を面接で尋ねました。

インターリーブがうまくいかないとデッドロックが発生すると答えましたが、インタビュアーはインターリーブに関係なく常にデッドロックになるプログラムを書くことができると主張しました。

そのようなプログラムを書くことはできますか?そのようなサンプルプログラムを教えていただけますか?


3
インタビュアーは間違いなく愚かな仲間です。
ライオン

23
インタビュアーは確かに愚かな仲間ではありません。主題を完全に理解するということは、極限のケースを説明できる必要があることを意味します。つまり、プログラムをロックしないようにし、常にロックするようにします。
ユーリー・ズバレフ2012

回答:


100

更新:この質問は、2013年1月の私のブログの主題でした。素晴らしい質問をありがとう!


スレッドがどのようにスケジュールされていても、常にデッドロックになるプログラムを作成するにはどうすればよいですか?

これがC#の例です。プログラムにはロックや共有データが含まれていないように見えることに注意しくださいローカル変数が1つとステートメントが3つしかないにもかかわらず、100%確実にデッドロックします。確実にデッドロックする、より単純なプログラムを思い付くのは難しいでしょう。

読者への演習#1:これがどのようにデッドロックするかを説明します。(答えはコメントにあります。)

読者への演習#2:Javaで同じデッドロックを示します。(答えはここにあります:https//stackoverflow.com/a/9286697/88656

class MyClass
{
  static MyClass() 
  {
    // Let's run the initialization on another thread!
    var thread = new System.Threading.Thread(Initialize);
    thread.Start();
    thread.Join();
  }

  static void Initialize() 
  { /* TODO: Add initialization code */ }

  static void Main() 
  { }
}

4
理論上のC#に関する私の知識は限られていますが、クラスローダーは、Javaの場合と同様にコードがシングルスレッドで実行されることを保証していると思います。JavaPuzzlersにも同様の例があると確信しています。
Voo 2012年

11
@Voo:あなたは良い記憶を持っています。「JavaPuzzlers」の共著者であるNealGafterと、数年前のオスロ開発者会議での「C#Puzzlers」の講演で、このコードの難読化されたバージョンを紹介しました。
Eric Lippert 2012年

41
@Lieven:静的コンストラクターは1回だけ実行する必要があり、クラス内の静的メソッドを最初に呼び出すに実行する必要があります。Mainは静的メソッドであるため、メインスレッドは静的ctorを呼び出します。1回だけ実行されるようにするために、CLRは、静的ctorが終了するまで解放されないロックを取得します。ctorが新しいスレッドを開始すると、そのスレッドも静的メソッドを呼び出すため、CLRはロックを取得して、ctorを実行する必要があるかどうかを確認しようとします。その間、メインスレッドはブロックされたスレッドに「参加」し、デッドロックが発生します。
Eric Lippert 2012年

33
@artbristol:私はJavaコードの行ほど多くを書いたことがありません。今から始める理由はありません。
Eric Lippert 2012年

4
ああ、私はあなたがあなたの演習#2への答えを持っていると思いました。Javaの質問に答えるためにこれだけ多くの賛成を得たことをおめでとうございます。
artbristol 2012年

27

ここでのラッチは、各スレッドが他のスレッドをロックしようとしたときに両方のロックが保持されることを保証します。

import java.util.concurrent.CountDownLatch;

public class Locker extends Thread {

   private final CountDownLatch latch;
   private final Object         obj1;
   private final Object         obj2;

   Locker(Object obj1, Object obj2, CountDownLatch latch) {
      this.obj1 = obj1;
      this.obj2 = obj2;
      this.latch = latch;
   }

   @Override
   public void run() {
      synchronized (obj1) {

         latch.countDown();
         try {
            latch.await();
         } catch (InterruptedException e) {
            throw new RuntimeException();
         }
         synchronized (obj2) {
            System.out.println("Thread finished");
         }
      }

   }

   public static void main(String[] args) {
      final Object obj1 = new Object();
      final Object obj2 = new Object();
      final CountDownLatch latch = new CountDownLatch(2);

      new Locker(obj1, obj2, latch).start();
      new Locker(obj2, obj1, latch).start();

   }

}

jconsoleを実行するのは興味深いことです。これにより、[スレッド]タブにデッドロックが正しく表示されます。


3
これはこれまでのところ最高ですがsleep、適切なラッチに置き換えます。理論的には、ここに競合状態があります。0.5秒で十分だとほぼ確信できますが、面接タスクにはあまり適していません。
2012年

25

デッドロックは、スレッド(またはプラットフォームが実行ユニットと呼ぶもの)がリソースを取得するときに発生します。各リソースは一度に1つのスレッドのみが保持でき、保持をプリエンプトできないようにそれらのリソースを保持します。デッドロック内の各スレッドが別のスレッドによって保持されているリソースの取得を待機するように、スレッド間には「循環」関係が存在します。

したがって、デッドロックを回避する簡単な方法は、リソースに全体的な順序付け、リソースはスレッドによってのみ順番に取得されるというルールを課すことです。逆に、デッドロックは、リソースを取得するスレッドを実行することによって意図的に作成できますが、リソースを順番に取得しません。例えば:

2つのスレッド、2つのロック。最初のスレッドは特定の順序でロックを取得しようとするループを実行し、2番目のスレッドは反対の順序でロックを取得しようとするループを実行します。各スレッドは、ロックを正常に取得した後、両方のロックを解放します。

public class HighlyLikelyDeadlock {
    static class Locker implements Runnable {
        private Object first, second;

        Locker(Object first, Object second) {
            this.first = first;
            this.second = second;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (first) {
                    synchronized (second) {
                        System.out.println(Thread.currentThread().getName());
                    }
                }
            }
        }
    }

    public static void main(final String... args) {
        Object lock1 = new Object(), lock2 = new Object();
        new Thread(new Locker(lock1, lock2), "Thread 1").start();
        new Thread(new Locker(lock2, lock1), "Thread 2").start();
    }
}

さて、この質問には、デッドロックの可能性確実性の違いを指摘するコメントがいくつかあります。ある意味で、その区別は学術的な問題です。実用的な観点から、私は確かに私が上で書いたコードでデッドロックしない実行中のシステムを見たいです:)

ただし、面接の質問は学術的な場合があり、このSOの質問のタイトルには「確かに」という単語が含まれているため、以下は確実に行き詰まるプログラムです。2つのLockerオブジェクトが作成され、それぞれに2つのロックが与えられCountDownLatch、スレッド間の同期に使用されます。それぞれLockerが最初のロックをロックしてから、ラッチを1回カウントダウンします。両方のスレッドがロックを取得してラッチをカウントダウンすると、ラッチバリアを通過して、2番目のロックを取得しようとしますが、いずれの場合も、もう一方のスレッドはすでに目的のロックを保持しています。この状況では、特定のデッドロックが発生します。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CertainDeadlock {
    static class Locker implements Runnable {
        private CountDownLatch latch;
        private Lock first, second;

        Locker(CountDownLatch latch, Lock first, Lock second) {
            this.latch = latch;
            this.first = first;
            this.second = second;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                first.lock();
                latch.countDown();
                System.out.println(threadName + ": locked first lock");
                latch.await();
                System.out.println(threadName + ": attempting to lock second lock");
                second.lock();
                System.out.println(threadName + ": never reached");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(final String... args) {
        CountDownLatch latch = new CountDownLatch(2);
        Lock lock1 = new ReentrantLock(), lock2 = new ReentrantLock();
        new Thread(new Locker(latch, lock1, lock2), "Thread 1").start();
        new Thread(new Locker(latch, lock2, lock1), "Thread 2").start();
    }
}

3
Linusの言葉を引用して申し訳ありませんが、「話は安いです。コードを見せてください。」—それは素晴らしい仕事であり、思ったよりも驚くほど難しいです。
アルフ

2
デッドロックなしでこのコードを実行することは可能です
Vladimir Zhilyaev 2012年

1
さて、皆さんは残忍ですが、これは今や完全な答えだと思います。
グレッグマット2012年

@GregMattesありがとう:)+ 1以外は追加できません。楽しんでいただければ幸いです:)
2012年

15

これは、EricLippertの例に従ったJavaの例です。

public class Lock implements Runnable {

    static {
        System.out.println("Getting ready to greet the world");
        try {
            Thread t = new Thread(new Lock());
            t.start();
            t.join();
        } catch (InterruptedException ex) {
            System.out.println("won't see me");
        }
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

    public void run() {           
        Lock lock = new Lock();      
    }

}

4
runメソッドでjoinを使用することは少し誤解を招くと思います。デッドロックが「newLock()」ステートメントによって引き起こされているときにデッドロックを取得するには、静的ブロック内の結合以外のこの他の結合が必要であることを示しています。C#の例のような静的メソッドを使用した私の書き直し:stackoverflow.com/a/16203272/2098232
luke657 2013

あなたの例を説明していただけますか?
gstackoverflow

私の実験によると、t.join(); 内側ラン()メソッドは冗長である
gstackoverflow

私は理解に防止冗長コード除去
gstackoverflow

11

ドキュメントのを次に示します。

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

2
+ 1Javaチュートリアルをリンクするため。
mre 2012年

4
「非常に可能性が高い」は「確実にデッドロックに陥る」には十分ではありません
2012年

1
@alfああ、でも根本的な問題はここで非常にうまく示されています。Object invokeAndWait(Callable task)メソッドを公​​開するラウンドロビンスケジューラを作成できます。その後、すべてCallable t1がしなければならないのはinvokeAndWait()Callable t2戻る前のその生涯の間であり、逆もまた同様です。
user268396 2012年

2
@ user268396ええと、基本的な問題は些細で退屈です:)タスクの全体的なポイントは、保証されたデッドロックを取得するのが驚くほど難しいことを見つけること、または理解していることを証明することです(非同期の世界で何かを保証することも同様です)。 )。
アルフ2012年

4
@bezzsleepは退屈です。5秒間スレッドが開始されないと私は信じていますが、とにかく競合状態です。sleep()競合状態の解決に依存するプログラマーを雇いたくない:)
2012年

9

EricLippertによって投稿されたデッドロックの例のYuriyZubarevのJavaバージョンを書き直しました:https://stackoverflow.com/a/9286697/2098232 C#バージョンにより近くなります。Javaの初期化ブロックがC#静的コンストラクターと同様に機能し、最初にロックを取得する場合、デッドロックを取得するためにjoinメソッドを呼び出すために別のスレッドは必要ありません。元のC#のように、Lockクラスから静的メソッドを呼び出すだけで済みます。例。結果として生じるデッドロックはこれを確認しているようです。

public class Lock {

    static {
        System.out.println("Getting ready to greet the world");
        try {
            Thread t = new Thread(new Runnable(){

                @Override
                public void run() {
                    Lock.initialize();
                }

            });
            t.start();
            t.join();
        } catch (InterruptedException ex) {
            System.out.println("won't see me");
        }
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

    public static void initialize(){
        System.out.println("Initializing");
    }

}

runメソッドでLock.initialize()をコメントアウトすると、デッドロックが発生しないのはなぜですか?しかし、初期化メソッドは何もしませんか?
aequitas 2015

@Aequitasは単なる推測ですが、メソッドは最適化されている可能性があります。それがスレッドでどのように機能するかわからない
Dave Cousineau 2016

5

それはあなたが得ることができる最も単純な面接タスクではありません:私のプロジェクトでは、それは一日中チームの仕事を麻痺させました。プログラムを停止させるのは非常に簡単ですが、スレッドダンプが次のような書き込みを行う状態にするのは非常に困難です。

Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 7f91c5802b58 (object 7fb291380, a java.lang.String),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 7f91c6075308 (object 7fb2914a0, a java.lang.String),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
    at uk.ac.ebi.Deadlock.run(Deadlock.java:54)
    - waiting to lock <7fb291380> (a java.lang.String)
    - locked <7fb2914a0> (a java.lang.String)
    - locked <7f32a0760> (a uk.ac.ebi.Deadlock)
    at java.lang.Thread.run(Thread.java:680)
"Thread-1":
    at uk.ac.ebi.Deadlock.run(Deadlock.java:54)
    - waiting to lock <7fb2914a0> (a java.lang.String)
    - locked <7fb291380> (a java.lang.String)
    - locked <7f32a0580> (a uk.ac.ebi.Deadlock)
    at java.lang.Thread.run(Thread.java:680)

したがって、目標は、JVMがデッドロックと見なすデッドロックを取得することです。明らかに、のような解決策はありません

synchronized (this) {
    wait();
}

彼らは確かに永遠に停止しますが、その意味で機能します。競合状態に依存することも良い考えではありません。インタビュー中は通常、ほとんどの場合機能するはずの何かではなく、確実に機能しているものを見せたいからです。

さて、sleep()解決策は、それが機能しないが公平ではない状況を想像するのは難しいという意味で大丈夫です(私たちは公正なスポーツをしていますね?)。@artbristolによる解決策(私のものは同じで、モニターとは異なるオブジェクトだけです)は素晴らしいですが、長く、新しい同時実行プリミティブを使用してスレッドを正しい状態にします。これはそれほど楽しいことではありません。

public class Deadlock implements Runnable {
    private final Object a;
    private final Object b;
    private final static CountDownLatch latch = new CountDownLatch(2);

    public Deadlock(Object a, Object b) {
        this.a = a;
        this.b = b;
    }

    public synchronized static void main(String[] args) throws InterruptedException {
        new Thread(new Deadlock("a", "b")).start();
        new Thread(new Deadlock("b", "a")).start();
    }

    @Override
    public void run() {
        synchronized (a) {
            latch.countDown();
            try {
                latch.await();
            } catch (InterruptedException ignored) {
            }
            synchronized (b) {
            }
        }
    }
}

私はそれをリコールを行うsynchronizedのみのソリューションは、(コメントや輸入を除く)コードの11..13ラインにフィットしますが、実際のトリックを思い出していません。更新します。

更新:これは醜い解決策synchronizedです:

public class Deadlock implements Runnable {
    public synchronized static void main(String[] args) throws InterruptedException {
        synchronized ("a") {
            new Thread(new Deadlock()).start();
            "a".wait();
        }
        synchronized ("") {
        }
    }

    @Override
    public void run() {
        synchronized ("") {
            synchronized ("a") {
                "a".notifyAll();
            }
            synchronized (Deadlock.class) {
            }
        }
    }
}

ラッチをオブジェクトモニター("a"オブジェクトとして使用)に置き換えることに注意してください。


うーん、それは公正な面接の仕事だと思います。Javaでのデッドロックとロックを本当に理解するように求められます。一般的な考え方もそれほど難しいとは思いません(両方のスレッドが最初のリソースをロックした後にのみ続行できることを確認してください)、CountdownLatchを覚えておく必要があります-しかし、インタビュアーとして、その部分でインタビュイーを支援します彼が必要なものを正確に説明できれば(ほとんどの開発者がこれまでに必要としたクラスではなく、インタビューでグーグルで検索することはできません)。面接でこんなに面白い質問をしてもらいたいです!
Voo 2012年

@Vooで遊んでいたとき、JDKにはラッチがなかったので、すべて手作業でした。そして、の違いLOCKEDとはwaiting to lock、あなたが朝食時に読んで何か微妙ではありません。しかし、まあ、あなたはおそらく正しいです。言い換えさせてください。
アルフ2012年

4

このC#バージョンでは、Javaはかなり似ているはずです。

static void Main(string[] args)
{
    var mainThread = Thread.CurrentThread;
    mainThread.Join();

    Console.WriteLine("Press Any key");
    Console.ReadKey();
}

2
いいね!consoleステートメントを削除した場合にデッドロックを作成するための本当に最短のC#プログラム。Main関数全体をThread.CurrentThread.Join();。と書くだけです。
RBT 2017年

3
import java.util.concurrent.CountDownLatch;

public class SO8880286 {
    public static class BadRunnable implements Runnable {
        private CountDownLatch latch;

        public BadRunnable(CountDownLatch latch) {
            this.latch = latch;
        }

        public void run() {
            System.out.println("Thread " + Thread.currentThread().getId() + " starting");
            synchronized (BadRunnable.class) {
                System.out.println("Thread " + Thread.currentThread().getId() + " acquired the monitor on BadRunnable.class");
                latch.countDown();
                while (true) {
                    try {
                        latch.await();
                    } catch (InterruptedException ex) {
                        continue;
                    }
                    break;
                }
            }
            System.out.println("Thread " + Thread.currentThread().getId() + " released the monitor on BadRunnable.class");
            System.out.println("Thread " + Thread.currentThread().getId() + " ending");
        }
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[2];
        CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(new BadRunnable(latch));
            threads[i].start();
        }
    }
}

各スレッドは他のスレッドのバリアで待機しているため、プログラムは常にデッドロックしますが、バリアを待機するには、スレッドがモニターをオンに保持している必要がありますBadRunnable.class


3
} catch (InterruptedException ex) { continue; }...美しい
artbristol 2012年

2

ここにJavaの例があります

http://baddotrobot.com/blog/2009/12/24/deadlock/

誘拐犯が現金を手に入れるまで被害者をあきらめることを拒否したときに行き詰まりに陥ったが、交渉担当者は犠牲者を手に入れるまで現金をあきらめなかった場合。


その実装は、与えられたように適切ではありません。一部のコードが欠落しているようです。ただし、デッドロックにつながるリソースの競合に関する限り、あなたが表現する一般的な考え方は正しいです。
マスターチーフ

この例は教育学的であるため、なぜ適切でないと解釈するのか不思議です...欠落しているコードは、メソッド名が役立つはずの空のメソッドです(ただし、簡潔にするために表示されていません)
Toby

1

簡単な検索で次のコードが得られました。

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

出典:デッドロック


3
「非常に可能性が高い」は「確実にデッドロックに陥る」には十分ではありません
2012年

1

これは、ロックを保持している1つのスレッドが同じロックを必要とする別のスレッドを開始し、スターターが開始が終了するまで待機するサンプルです...永久に:

class OuterTask implements Runnable {
    private final Object lock;

    public OuterTask(Object lock) {
        this.lock = lock;
    }

    public void run() {
        System.out.println("Outer launched");
        System.out.println("Obtaining lock");
        synchronized (lock) {
            Thread inner = new Thread(new InnerTask(lock), "inner");
            inner.start();
            try {
                inner.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class InnerTask implements Runnable {
    private final Object lock;

    public InnerTask(Object lock) {
        this.lock = lock;
    }

    public void run() {
        System.out.println("Inner launched");
        System.out.println("Obtaining lock");
        synchronized (lock) {
            System.out.println("Obtained");
        }
    }
}

class Sample {
    public static void main(String[] args) throws InterruptedException {
        final Object outerLock = new Object();
        OuterTask outerTask = new OuterTask(outerLock);
        Thread outer = new Thread(outerTask, "outer");
        outer.start();
        outer.join();
    }
}

0

次に例を示します。

2つのスレッドが実行されており、それぞれが他のスレッドがロックを解放するのを待っています

パブリッククラスThreadClassはThread {を拡張します

String obj1,obj2;
ThreadClass(String obj1,String obj2){
    this.obj1=obj1;
    this.obj2=obj2;
    start();
}

public void run(){
    synchronized (obj1) {
        System.out.println("lock on "+obj1+" acquired");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("waiting for "+obj2);
        synchronized (obj2) {
            System.out.println("lock on"+ obj2+" acquired");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


}

}

これを実行すると、デッドロックが発生します。

パブリッククラスSureDeadlock {

public static void main(String[] args) {
    String obj1= new String("obj1");
    String obj2= new String("obj2");

    new ThreadClass(obj1,obj2);
    new ThreadClass(obj2,obj1);


}

}

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.