问答:复杂的死锁评估测试以及如何评估答案

时间:2013-11-14 21:57:58

标签: java multithreading

为测试构建了以下代码。在此测试中,要求读者解释为什么代码在启动代码后不到一秒就会进入死锁状态。

任何人都可以完全描述导致此代码死锁的原因吗?

public class Test {

  static class FailerThread implements Runnable {

    final Object[] objects;
    final Random random;
    final int number;

    public FailerThread(final Object[] objects, final int number) {
      this.objects = objects;
      this.random = new Random();
      this.number = number;
    }

    @Override
    public void run() {
      final boolean isWriter = number % 2 == 0;
      int index = random.nextInt(objects.length);
      try {
        while (Thread.interrupted() == false) {
          synchronized (objects) {
            if (isWriter) {
              while (objects[index] == null) {
                System.out.println(number + ": Index " + index + " is null, waiting...");
                objects.wait();
              }
              for (int copyIndex = 0; copyIndex < objects.length; ++copyIndex) {
                if (objects[copyIndex] == null) {
                  objects[copyIndex] = this.objects[index];
                }
              }
              objects.notifyAll();
            } else {
              objects[index] = null;
            }
          }

          ++index;
          if (index >= objects.length) {
            index = 0;
          }
        }
      } catch (InterruptedException e) {
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    final Object[] objects = new Object[10];
    for (int i = 0; i < objects.length; ++i) {
      objects[i] = new Object();
    }

    final int NUM_THREADS = 32;
    final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
    for (int i = 0; i < NUM_THREADS; ++i) {
      executor.execute(new FailerThread(objects, i));
    }
  }
}

编辑:此测试的官方答案(类似于都铎所写,更详细)

上面构造了死锁,因为在某些时候所有&#34;编写者&#34;等待null,但由于那些编写者是唯一可以释放它们的人,他们将无限期地挂起。然而,更重要的问题是:为什么?

乍一看,代码看起来像那些作家占主导地位。每个循环选择一个线程(writer或nuller)来处理数组,但是当nuller只写一个null时,writer会删除数组中的所有空值。因此人们可能会认为,尽管可能的话,死锁的可能性很小(但令人惊讶的是,代码在一秒钟内死锁)。仔细看看,这个假设结果是错误的,因为我们正在处理线程。

如果有足够的执行时间,在多线程应用程序中重要的是:代码的哪一部分实际上能够阻止?让我们来看看编写器/零程序可能出现的最坏情况:

  • 在最坏的情况下,空洞可以执行而不会产生任何影响。即:它将null写入数组中已经为空的位置。

  • 作家可以 - 在最坏的情况下 - 无限期地阻止。

此外,在同步块的开始处,选择(或多或少)随机候选者进入。在开始时,对于作家和傻瓜来说,这都是50%,但是对于每个被封锁的作家来说,这些机会都有利于nuller的方向。即使成功的写入消除了所有空值,但是nuller的机会总是50%或更多,因为编写者(由于阻塞)的机会不断减少。因此,从线程角度来看,nuller实际上是主要部分,因为整个系统被设计为支持它们作为同步块的候选者。

此外 - 这是重要的部分 - 线程的执行顺序未定义。一个天真的印象是允许哪个线程执行交替,但事实并非如此。同步块没有首选项,并且未定义哪个线程获得访问权限(可以说:完全随机,尽管不涉及随机)。因此,在所有16个线程等待同步的情况下,在20个执行线程中完全交替的机会,恰好等于连续调用20个写入器或20个零线程的机会。但由于nullers占主导地位(20个编写器什么都不做),连续调用20个nuller几乎可以保证将整个数组设置为null,这会导致任何后续编写器无限期地阻塞。

如果向代码添加更多日志记录输出以查看实际选择了哪个线程,您很快就会看到连续调用10个或更多nuller的内容,通常在前200个循环中。在那之后,系统就会挂起。

为什么要问这个问题

我目前正在开发一个用于评估专家Java程序员的测试集,并且所有编写的代码最终都需要进行测试。好消息:它成功了。 ;)

现在,在您抱怨StackOverflow使用不当之前:请将此视为Q&amp; A。对于多线程体系结构的实际实现,这个例子还有很多值得学习的地方。由于这是一个专家级问题 - 正如预期的那样 - 没有多少人能够回答它,甚至不能理解它。然而,关于专家级问题的好处是,您可以从专家级答案中学到很多东西。这就是为什么我要包含完整详细的答案。

候选人的评分方式

预计有些人会认为这个问题对评估测试来说太难了,并且给出了测试人员的观点,这就是候选人的评分方式:

是的,问题太难了,没有人希望在测试过程中找到正确的答案,重要的是他们如何解决问题。每天程序员都会遇到他/她以前从未解决的任务,并且不知道如何立即解决,因此在解决问题方面具备良好的技能是这项业务的重点。没有人能够知道一切,但每个人都可以学习。

一般来说,有4种可能的结果:

  1. 候选人不知道答案并且这样说。这是一个很好的初学者水平,因为候选人有能力承认在紧张的测试情况下。一个好学生是倾听的,因此可以被教导。

  2. 候选人现在知道答案,但要么指责“坏”&#34;问题(也就是投票)或者得出错误的答案,他/她然后疯狂地辩护。这基本上是最糟糕的候选人:他处于初级/中级水平,但他认为自己是专家,因此拒绝学习并将被困在这个级别。在一个团队中,这个候选人要么阻止团队的进步(如果他们认为他是&#34;专家&#34;)或者很快就会成为一个麻烦。

  3. 候选人提出(或多或少正确)答案,并使用有条不紊的方法来找到它。这是一个很好的中级/专家级候选人。他/她已经开发出一种有条理的方法来挑战任务,并且可以根据答案进一步推进。

  4. 候选人采用有条不紊的方法,并提出正确的答案。这是最好的结果,但可能只有百万分之一。

3 个答案:

答案 0 :(得分:0)

不知道这是否是您期望的答案,但我可以看到在满足这两个条件时发生的死锁:

  1. 至少10名“读者”(非作家)能够进入 连续同步块,不允许任何编写器 继续进行。
  2. 每个数组索引从0到9由一个通过锁定的读者随机拾取至少一次。
  3. 由于您有16个读者和16个写入者并且上述适用,10个读取器在随机数生成器上拾取0到9可以使整个数组为空,从而导致所有编写器被阻塞,因为它们的相应索引由他们可以到达while循环的时间。

    编辑:事实上,它甚至更简单:10位读者甚至不需要连续进入锁定。如果K读者进入锁定,并且在数组中使用0 < i <= K(因为索引可以重叠)进入null i位置,那么如果在它们之后进入的编写者都具有以前读者使用的集合中的索引,那么将被阻止。由于读者最终将使整个数组无效,如果所描述的情况重复,则可能导致所有作者在有限的迭代次数内阻塞。

答案 1 :(得分:-1)

它不是 deadlock ,因为所有线程只有一个同步化资源。

简单地说 deadlock 当两个线程需要两个资源用于某些操作时,一个线程抓取第一个资源,第二个线程抓取第二个资源,两者都不能继续。

在你的情况下,只是所有线程都陷入无限睡眠或无限循环。

代码中的所有线程分为两组,我将它们称为“编写器”(那些使用isWrite == true)和“nullifiers”(从技术上讲,它们也会编写,但它们总是写为null)。

在某些时候,“nullififer”线程遍历数组并将所有元素设置为null。它可以通过一个“nullifier”线程来完成,因为在将一个设置为null之后,没有什么能阻止它继续进行到数组的下一个元素,以及在几次迭代过程中没有任何阻止它继续下一个元素。

没有“writer”可以继续进行,因为当他们当前元素为null时,他们会执行objects.wait()。所以他们陷入了无限的睡眠。

所有“nullifier”线程一遍又一遍地无限覆盖带有空值的数组。

即使起初“writer”线程获得更多处理器时间,最终它们也会被“nullifiers”覆盖,因为“writers”有停止条件,而“nullifiers”没有。

UPDATE:顺便说一句,您不必在“if”语句中执行布尔比较。 你可以写

 while (!Thread.interrupted())

哪种更易读,更简洁,是一种常见做法。

更新2:您可以尝试通过在“nullifier”的else语句中添加objects.wait()来修复,类似于“writer's”if子句中的那个,如下所示:

} else {        
    objects[index] = null;
    while (objects[index]==null) {
        System.out.println(number + ": Index " + index + " is still null, waiting...");
        objects.wait();
    }
}

我不知道这段代码应该完成什么(它看起来就像是一些随机练习),所以我不确定解决方案是否在语义上是正确的,但它应该解决“横冲直撞”问题。

更新3:如果在循环开始时添加线程类型的日志记录,您可以很快看到只有运行的线程是那些带有isWriter == false的线程。 编辑:最好在同步后进行日志记录

while (Thread.interrupted() == false) {
    synchronized (objects) {
        System.out.println("running isWriter=" + isWriter + " thread #" + number);

答案 2 :(得分:-1)

&#34;官方回答&#34;那个问号发布者在很多方面都是错误的,我将为那些将来偶然发现这个问题的人发布另一个答案。

在有人开始争辩说我是最糟糕的情况之前&#34;候选人,他&#34;疯狂地捍卫错误的答案&#34; (如海报暗示):

  • 我进行了日志记录和断点调试(显然与原始版本不同) 海报,或者他可能没有足够的时间来做这件事。)
  • 我根本不是候选人,我只是试图按预期使用Stackoverflow,提供最好的答案,如果不是为了受害者的话,那么将会访问的其他人
  • 我这样做是因为从原始海报的回答中别人会得到 不正确的同步概念。
  • 虽然具有讽刺意味的原创性,但我并没有贬低问题 海报downvoted我的答案

更不用说回答&#34;专家&#34;级错误的基本假设问题不会给你正确答案。

同步

在答案海报中多次提到nuller只写了一个值:

  

但是当一个nuller只写一个null时,一个writer会删除所有   数组中的空值。

     

在最坏的情况下,空洞者可以在没有任何影响的情况下执行。那是:   它将null写入数组中已经为空的位置。

代码中没有任何内容可以证明这一点。

离开同步条款不要让线程放弃执行另一个线程。

同步的唯一目的是保证没有两个线程同时进入同一个临界区,基本上它只是一个互斥锁。

Intrinsic Locks and Synchronization

Mutual exclusion

java中有几种方法可以进行线程停止控制:

  • Thread.yield() - 提示 jvm,它可以执行 另一个线程。基本上它对jvm &#34;我在这里做了很多工作, 如果你愿意,你可以给其他人一些处理器时间,但是我 也可以继续。&#34;
  • Thread.sleep() - 在一段固定的时间内暂停线程。
  • Object.wait() - 暂停线程,直到有人在同一个对象上调用notify(只唤醒一个服务员)/ notifyAll(唤醒所有服务员)。
  • 还有其他人,但他们是相似的,通常基于这些

但是没有一个用于nullers! 因此,绝对不能保证nuller只会写一个值。

事实上,由于一次迭代的计算强度较低,因此在执行窗口期间线程接收它更有可能进行多次迭代。

但是你不必相信我,只需要记录和调试。没有实际测试,这只是一个蛊惑人心的事情。 (更新:请注意,日志记录应位于同步块内)

Screenshot with output of the program, showing how nuller resets several values in a row then one writer overwrites all values and then nuller resets all elements, leading to the problem described in question 请注意,第一个nuller连续重置几个值而不被写入器中断(与海报所说的相反),然后编写器覆盖数组,然后nuller重置所有数组(时间上有几个元素),而写入者则进入睡眠状态,导致描述问题

更新:此外作者说

  

另外 - 这是重要的部分 - 线程的执行顺序是未定义的。一个天真的印象是允许哪个线程执行交替,但事实并非如此。

然而,自己却成了那种天真的印象&#34;期望在执行nuller之后,非常线程不能进入同步。

概率驱动的发展

  

即使成功的写入消除了所有空值,也有机会   nuller总是50%或更多

这只会将整个概率理论抛到窗外,更不用说基于概率编写代码真是个坏主意,特别是当它涉及多线程和长执行时间时(infinite monkey theorem)。

虽然在一件事情中作者是对的:随着时间的推移,会有更多的空洞和更少的作家。

UPDATE:之所以重要,是因为你根本不应该谈论多线程的概率,因为如果某些情况是可能的,无论它在某些情况下会发生多么不可能点。

多线程应该根据最坏的情况来讨论,就像作者刚开始时一样,尽管他未能确定nuller的实际最坏情况,即当单个nuller线程在没有被中断的情况下迭代所有数组并将所有值设置为空。

更新:要了解错误的50/50机会如何分割,请考虑以下简化示例:

假设我们有两个线程,我们没有为它们设置优先级,因此默认情况下它们都有java.lang.Thread.NORM_PRIORITY。调度程序将尝试在它们之间或多或少地平均分配处理器时间。

然而,一个线程迭代一个大数组,并且需要一分钟才能这样做 而另一个只设置数组的单个元素,它需要一秒钟 它们都在一个对象上同步,因此它们不能同时执行。

在begging中,调度程序控制第一个线程并开始迭代数组,即使调度程序将尝试中断它并给第二个线程留一些时间,第二个线程也无法继续,因为已经获得了第一个线程锁。

因此,当分钟传递和第一个线程释放锁定时,调度程序决定对他来说足够了,并且它必须给第二个线程一分钟左右的时间,因为它们都具有相同的优先级,但因为它的第二个线程关键部分只需要一秒钟,然后可以输入~60次。

当然示例是简化的并且会有抖动,有时调度程序会给出不相等的时间块,但总体而言,它会尝试根据优先级为线程提供处理器时间量。

因此推理说,在50%的情况下,作家会进入关键部分,因为作家和傻瓜的数量相等。类似于一则古老的轶事:

  

- What's the probability that leaving your home you will see an alive dinosaur?
  - 50% either I will see it or not.