编写一个肯定会陷入僵局的程序[关闭]


86

我最近在一次采访中提出了这个问题。

我回答说,如果交织出错,就会发生死锁,但是访调员坚持认为,可以编写一个无论交织而总是陷入死锁的程序。

我们可以编写这样的程序吗?您能指出我这样的示例程序吗?


3
面试官绝对是一个愚蠢的家伙。
狮子

23
面试官当然不是一个愚蠢的家伙。完全理解一个主题意味着您应该能够解释极端情况:使程序永不锁定,并始终锁定。
Yuriy Zubarev

Answers:


100

更新:这个问题是我2013年1月博客的主题。感谢您提出的好问题!


无论线程如何调度,我们如何编写一个始终会陷入死锁的程序?

这是C#中的示例。请注意,该程序似乎不包含锁和共享数据。它只有一个局部变量和三个语句,但死锁具有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中那样是单线程运行的。我很确定Java Puzzlers中也有类似的例子。
Voo 2012年

11
@Voo:你记性很好。Neal Gafter –“ Java Puzzlers”的合著者–几年前,我在奥斯陆开发者大会上的“ C#Puzzlers”演讲中介绍了该代码的混淆版本。
埃里克·利珀特

41
@Lieven:静态构造函数必须运行不超过一次,并且必须第一次调用类中的任何静态方法之前运行。Main是静态方法,因此主线程调用静态ctor。为了确保它只运行一次,CLR会取出一个锁定,直到静态ctor完成后才会释放。当ctor启动新线程时,该线程还会调用静态方法,因此CLR尝试获取该锁以查看是否需要运行ctor。同时,主线程“加入”了被阻塞的线程,现在我们陷入了僵局。
埃里克·利珀特

33
@artbristol:我从来没有写过那么多的Java代码。我认为没有理由现在开始。
埃里克·利珀特

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很有意思,它将在Threads选项卡中正确显示死锁。


3
到目前为止,这是最好的,但是我将替换sleep为适当的闩锁:理论上,我们这里有一个竞争条件。虽然我们几乎可以肯定0.5秒就足够了,但对于面试任务来说并不太好。
2012年

25

死锁发生在线程(或平台所调用的执行单元)获取资源时,其中每个资源一次只能由一个线程持有,并以不能抢占的方式持有这些资源,并且线程之间存在某种“循环”关系,因此死锁中的每个线程都在等待获取另一线程持有的某些资源。

因此,避免死锁的一种简单方法是对资源进行总体排序,并强加一个规则,即只有线程才能按顺序获取资源。相反,可以通过运行获取资源但不按顺序获取资源的线程来故意创建死锁。例如:

两个线程,两个锁。第一个线程运行一个尝试以某种顺序获取锁的循环,第二个线程运行一个尝试以相反顺序获取锁的循环。成功获取锁后,每个线程都释放两个锁。

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问题的标题中确实有“肯定”一词,因此,随之而来的是一个肯定会陷入僵局的程序。Locker创建两个对象,每个对象都有两个锁,一个CountDownLatch用于在线程之间进行同步。每个Locker锁先锁定第一个锁,然后将锁存器递减一次。当两个线程都已获取锁并递减计数时,它们会越过闩锁屏障并尝试获取第二个锁,但是在每种情况下,另一个线程已经拥有了所需的锁。这种情况导致一定的僵局。

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的话:“对话很便宜。请给我看代码。” —这是一项不错的任务,而且比看起来要难得多。
2012年

2
可以在没有死锁的情况下运行此代码
Vladimir Zhilyaev 2012年

1
好的,你们很残酷,但是我认为这是一个完整的答案。
格雷格·马特斯

@GregMattes谢谢:)除了+1不能添加任何东西,希望您玩得开心:)
2012年

15

这是一个遵循Eric Lippert的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
我认为在运行方法中使用Join几乎不会引起误解。这表明,除了死锁是由“ new Lock()”语句引起的之外,还需要除静态块中的那个之外的其他连接。我用C#示例中的静态方法重写:stackoverflow.com/a/16203272/2098232
luke657 2013年

您能解释一下您的例子吗?
gstackoverflow

根据我的实验t.join(); 内部run()方法是多余的
gstackoverflow

我删除了妨碍理解的冗余代码
gstackoverflow '19

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
+1用于链接Java教程。
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

我已经重写了埃里克·利珀特(Eric Lippert)发布的Yuriy Zubarev的Java版本的死锁示例:https//stackoverflow.com/a/9286697/2098232,使其与C#版本更加相似。如果Java的初始化块的工作方式类似于C#静态构造函数,并且首先获取了锁,我们不需要另一个线程来调用join方法来获取死锁,则它只需要调用Lock类中的某些静态方法,例如原始的C#例。由此产生的死锁似乎可以证实这一点。

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 16/02/22

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-only解决方案适合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中没有闩锁,因此全部是手工完成的。之间的差异LOCKEDwaiting 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

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 2012年

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

这是一个示例,其中一个持有锁的线程启动了另一个想要相同锁的线程,然后启动器一直等到启动完成...永远:

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

这是一个例子:

两个线程正在运行,每个线程都在等待对方释放锁

公共类ThreadClass扩展了线程{

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);


}

}

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.