【问题标题】:Can two threads waiting on the same monitor be called a deadlock?在同一个监视器上等待的两个线程可以称为死锁吗?
【发布时间】:2016-05-28 17:34:38
【问题描述】:

两个线程在同一个监视器上等待,例如,如果一个线程调用 wait on 'lock',而另一个获取监视器的线程也在通知第一个线程之前调用 wait。现在两个线程都在等待,但没有人收到通知。我该怎么称呼这种情况?这能叫死锁吗?

编辑: 假设这是仅有的两个线程,并且无法从其他地方通知它们。
更新:我刚刚创建了我所描述的情况。当更改器线程在侦听器线程之前启动时,以下代码大部分时间都可以正常工作。但是,当我在转换器之前启动侦听器时,程序在打印两行(一行来自转换器,另一行来自侦听器线程)后挂起。我在 changer 之前调用 listener 的情况会被称为死锁吗?

package demo;

public class ProducerConsumer {

public static int SAMPLE_INT = 0;

public static void main(String[] args) {

    PC pc = new PC();

     Thread changer = new Thread(new Runnable() {
        public void run(){
            try {
                pc.producer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
     });

    Thread listener = new Thread(new Runnable(){
        public void run() {
            try {
                pc.consumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    changer.start();
    listener.start(); 
   }
 }

class PC {

Object lock = new Object();

public void producer() throws InterruptedException {
    synchronized(this){
        for (int i=0; i<5; i++){
            ProducerConsumer.SAMPLE_INT++;
            System.out.println("Changed value of int to: " + ProducerConsumer.SAMPLE_INT);
            wait();
            notify();
           }
    }
}

public void consumer() throws InterruptedException{
    synchronized(this){
        for (int i=0; i<5; i++){
            System.out.println("Receieved Change: " + ProducerConsumer.SAMPLE_INT);
            notify();
            wait();
           }
         }
       }
     }


在监听器之前启动转换器时的输出:
将 int 的值更改为:1
收到的零钱:1
将 int 的值更改为:2
收到的零钱:2
将 int 的值更改为:3
收到的零钱:3
将 int 的值更改为:4
收到的零钱:4
将 int 的值更改为:5
收到的更改:5
程序终止。

在更改程序之前启动侦听器时的输出:
收到的零钱:0
将 int 的值更改为:1
程序不会终止。

谢谢。

【问题讨论】:

  • 你创建的锁对象没有被使用。此外,当调用'wait'时,它应该在一段时间内(!conditionMet){lock.wait(); ... }。使用同步方法/块和调用等待/通知有许多细微差别。继续阅读,你会更清楚。至于您的问题,正如我们在下面的回答中进行了辩论,由您决定。我和 Krashimir 说这不是僵局,而安迪和其他人说这是僵局。通读讨论并自己决定:) 干杯!
  • 没有人拥有“死锁”这个词,你可以随心所欲地使用它,无论你如何使用它,都会有有人抱怨你在使用它它错了。 IMO,他们都会接受的最广泛的定义至少部分正确是:一组线程,其中没有一个成员能够取得进展,直到至少一个其他成员取得进展。跨度>

标签: java multithreading deadlock


【解决方案1】:

我认为 Krasimir 和 Andy 都给出了正确的答案。我想提供一个额外的证明(有点),这种情况在技术上不是死锁。

这是因为该程序上的 Java 线程转储没有报告这是一个死锁。尽管 Java 线程转储可能会误报,但我们可以假设如果出现死锁,线程转储可以很好地报告它。

所以,我用listener (consumer)changer (producer) 之前开始运行你的程序在程序的文本顺序中,和你一样,我收到了挂起。此时,我得到了一个线程转储(有多种方法可以做到,看你的 IDE 是最简单的)。线程转储的相关部分在这里 [1]。

如果这是一个死锁,线程转储会明确说明(类似于发现一个 Java 级别的死锁)。但既然没有,我们可以放心,这不是死锁。

查看线程转储(每个线程的“堆栈”转储)仍然非常很有启发性。您可以看到生产者和消费者线程都首先锁定了锁&lt;0x000000076abca250&gt;(进入了监视器),正如lock ... 行所建议的那样。奇怪的是(或不是),他们都在同一个锁上等待(正如waiting on ... 所建议的那样)线!

这就是输入thread's wait set 的微妙之处。为了让线程等待锁,它必须首先放弃同一个锁。

这正是consumer#wait 所做的(下面转储中的第二个线程):放弃排他锁,以便其他线程实际上可以抓住它,并希望通过notify 唤醒他。

好的,consumer 现在已退出对该锁的争用。请注意,consumerwait 之前已经完成的notify 调用可能会也可能不会误入歧途。如果是这样,那么我们就面临着我们试图推理的悬而未决的情况。

与此同时,生产者线程也开始运行(可能稍晚一些)。由于消费者为了等待而“放弃”了锁,生产者抓住了锁,它现在也执行wait。就像消费者一样,现在它正在在同一个锁上等待,同样希望有人会过来唤醒他!

同样,从技术上讲,这两个线程都没有独占锁定任何锁。由于两个线程都在等待,它们已经放弃了锁,如果有其他线程出现(太糟糕了,在这种情况下不是!),它们的希望可能会实现。但是在这种情况下,程序将永远挂起,因为没有其他任何东西可以改变这种情况(JVM 挂起的原因在于线程不是守护进程,但这是另一个细节)。因此,wait-notify 是 Java 中用于线程通信的低级原语,它们的适当使用是通过所谓的条件变量。这就像线程承认它们正在处理一个共享资源,并且它们通过这些原语在任何边界情况(例如,共享队列为空的情况下)相互通信。

如果一个线程实际上持有一个锁L1(即它正在synchronized部分内执行)并且突然,它需要抓住另一个锁L2来执行它的任务,同时另一个线程同时持有L2,正在寻找抢 L1,然后发生死锁。由于这里不是这种情况,所以这不是死锁。

话虽如此,我有一些建议可以改进此代码。正如 Joshua Bloch 所说,“示例代码必须具有示范性”:

  1. PC 类中删除Object lock = new Object(); 行。它没有任何目的。你没有锁定它。您正在锁定与 PC 实例关联的监视器:PC pc = new PC();
  2. 考虑在public static int SAMPLE_INT = 0; 中将SAMPLE_INT 重命名为count 改名为volatile。不保证对此变量所做的更改对另一个线程可见
  3. SAMPLE_INT不需要在ProducerConsumer中定义,在PC中定义即可。
  4. 就像有人建议的那样,考虑在启动线程时使用CountdownLatch

[1] 线程转储(我使用了tmp 包名):

2016-02-23 15:02:49
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.74-b02 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=31 tid=0x00007f80b381c800 nid=0x1303 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f80b480b000 nid=0x5903 in Object.wait() [0x00000001294e5000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076abca250> (a tmp.PC)
    at java.lang.Object.wait(Object.java:502)
    at tmp.PC.producer(ProducerConsumer.java:44)
    - locked <0x000000076abca250> (a tmp.PC)
    at tmp.ProducerConsumer$1.run(ProducerConsumer.java:14)
    at java.lang.Thread.run(Thread.java:745)

"Thread-1" #12 prio=5 os_prio=31 tid=0x00007f80b6801000 nid=0x5703 in Object.wait() [0x00000001293e2000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076abca250> (a tmp.PC)
    at java.lang.Object.wait(Object.java:502)
    at tmp.PC.consumer(ProducerConsumer.java:55)
    - locked <0x000000076abca250> (a tmp.PC)
    at tmp.ProducerConsumer$2.run(ProducerConsumer.java:24)
    at java.lang.Thread.run(Thread.java:745)

【讨论】:

    【解决方案2】:

    您无法使用此代码控制哪个线程将首先获得锁。如果消费者先获取,它对 notify() 的调用不会唤醒任何线程,它会等待离开锁。现在生产者获取锁,生产并等待。 这是一个一般的竞争条件问题,您需要控制哪个线程应该首先开始执行(在这种情况下为生产者) 您可以使用 CyclicBarrier 来控制线程调用的顺序。

    【讨论】:

      【解决方案3】:

      这不是死锁,因为有办法摆脱这种情况 - 如果第三个线程调用 notify()notifyAll(),那么前两个等待线程将返回到就绪状态。

      死锁通常无法在应用程序本身内解决,需要重新启动。

      这就是为什么我不会把你的情况称为僵局。

      还有另外两个术语可以描述线程协调问题:

      活锁饥饿

      这里是 LoveLock 和 Starvation 的确切定义 - Starvation and LiveLock

      这也不是 LiveLock,因为线程不会相互响应。

      您的情况可能与 饥饿 一词最接近,但并不完全是饥饿。饥饿是指线程等待已占用很长时间的资源。在您的情况下,资源是对象的锁,永远不会再被获取。所以你最好的镜头是“An Endless Starvation”。

      我个人将其称为“生产者-消费者错误”,因为等待通知机制描述和管理“生产者-消费者”模式的线程协调,而这种方法(无通知等待)只是开发人员错误或方法的误用。

      【讨论】:

      • 我编辑了这个问题,所以在这种情况下我可以称之为死锁还是这种情况有其他名称?
      • 应该调用什么取决于其他情况。例如,如果这两个线程是线程池的一部分,并且如果池中有第三个线程,它可能会调用notify,那么这是线程饥饿的情况。这归结为为什么
      • 我编辑了我的答案,以便您获得更清晰的定义检查
      【解决方案4】:

      死锁基本上意味着一个线程持有一个锁(第一个锁),然后想要获得另一个锁(第二个锁),它永远无法获得,因为第二个锁被另一个想要获得第一个锁的线程持有.这也可能发生在线程链中,例如,Thread1 有 LockA,Thread2 有 LockB,Thread3 有 LockC,它们正在等待其他线程持有的锁(例如,Thread1 想要 LockB,Thread2 想要 LockC,而 Thread3锁A)。在这种情况下,任何线程都无法继续进行。

      只有这种情况才称为死锁;持有锁的线程等待另一个锁,除非它释放它的锁,否则永远不会获得。

      在锁对象上调用wait 从技术上讲会释放线程持有的锁。

      所以,为了回答你的问题,我认为你不能将你在问题中提到的场景称为死锁。

      【讨论】:

      • 非常字面解释 - 但从技术上讲,如果一个线程在等待另一个资源时阻塞,反之亦然,不管监视器是锁定还是解锁 - 它仍然是死锁。还没有看到很多关于死解锁的帖子;-) 不过活锁很有趣。
      • 是的,您所描述的正是我想象中的死锁,但是根据 Oracle 的 Java 文档,“死锁描述了两个或多个线程永远被阻塞、相互等待的情况。”那么我所描述的情况不能符合这个定义吗?如果不是死锁,这种情况有什么名字吗?
      • 这正是您所描述的 - 它们都阻塞在等待对方。这与说两个线程的监视器都锁定不同。当您调用 wait 时,监视器变为 解锁,线程变为 阻塞,同时等待永远不会出现的 notify
      • @S.Doe,正如 Oracle 的文档所说,死锁一词是指线程永远被阻塞,等待彼此。在这里,两个线程不等待彼此。他们只是在等待另一个线程。因此,您可以肯定地说这不是死锁。至少恕我直言。
      • @Andy,假设一个线程正在等待使用 ServerSocket#bind 的套接字连接,并且没有客户端出现,因为整个应用程序是单线程的,并且没有其他客户端可以#connect这个套接字,你会称它为死锁吗?仅仅因为这个线程在技术上等待另一个线程调用#connect?
      【解决方案5】:

      @S.Doe 通过编辑您的问题,情况是死锁,因为有 2 个线程并且都在等待队列中并且没有在其上执行任何代码。 也没有第三个线程,这意味着甚至没有线程处于可运行/运行状态。这是一个僵局。

      【讨论】:

      • 这不是死锁。当我们编写涉及“等待”的代码时,我们应该让对应的“通知”。通常它是“生产者-消费者”模式。调用“等待”会释放锁,并且从技术上讲,监视器/lockObject 没有被锁定。
      • 这是我们遵循的推荐准则。但在这里他问的是不同的东西,在他的上下文中,这是一个僵局。
      • 我刚刚注意到,他实际上更新了问题。让我新鲜回答。谢谢
      【解决方案6】:

      对于这种情况,最好使用 wait(timeout) 方法而不是 wait()。 即使没有来自其他线程的通知,它也会在超时时间后释放锁

      【讨论】:

        【解决方案7】:

        如果这些是唯一涉及的两个线程(例如访问监视器),那么是的。如果有其他线程可以访问监视器并解锁它,那么没有。

        但是请记住,您正在谈论两个主题 - 监视器通常是线程术语中的互斥锁。但是wait 是与 条件变量 相关联的东西,它虽然需要互斥锁才能工作,但会执行一项更微妙的任务,即根据 条件 故意阻塞线程线程向另一个发出信号,并带有称为 spurious wakeups 的警告。

        【讨论】:

        • 我假设这些是唯一涉及的两个线程。但是死锁通常不是两个线程等待获取彼此监视器但不能的情况。因此,如果只有一个监视器和两个线程,但由于两者都在等待,没有人持有监视器,将这种情况称为死锁是否正确?
        • 是的。这就是死锁的经典定义。
        • 两个线程不能在没有另一个线程参与的情况下在监视器上等待。线程不会在它拥有的监视器上等待,所以如果它等待它必须被另一个线程锁定。
        • @ThomasStets - 线程必须 wait 在它拥有的监视器上,否则它不能保证谓词(即正在等待的条件)受到真正的保护.其他任何东西都是错误。
        • @ThomasStets,补充一下 Andy 提到的内容,如果线程在它不持有的监视器/lockObject 上调用“等待”,它将引发运行时异常。线程可以而且应该只“等待”它在同步上下文中拥有的锁对象。
        猜你喜欢
        • 1970-01-01
        • 2018-10-07
        • 2011-11-01
        • 1970-01-01
        • 2015-02-02
        • 1970-01-01
        • 1970-01-01
        • 2013-12-13
        • 2012-03-21
        相关资源
        最近更新 更多