我有一个没有响应的应用程序,似乎处于死锁或类似死锁的状态。请参阅下面的两个线程。注意,My-Thread@101c线程阻止了AWT-EventQueue-0@301。但是,My-Thread刚刚称为java.awt.EventQueue.invokeAndWait()。因此,AWT-EventQueue-0阻止My-Thread(我相信)。

My-Thread@101c, priority=5, in group 'main', status: 'WAIT'
     blocks AWT-EventQueue-0@301
      at java.lang.Object.wait(Object.java:-1)
      at java.lang.Object.wait(Object.java:485)
      at java.awt.EventQueue.invokeAndWait(Unknown Source:-1)
      at javax.swing.SwingUtilities.invokeAndWait(Unknown Source:-1)
      at com.acme.ui.ViewBuilder.renderOnEDT(ViewBuilder.java:157)
        .
        .
        .
      at com.acme.util.Job.run(Job.java:425)
      at java.lang.Thread.run(Unknown Source:-1)

AWT-EventQueue-0@301, priority=6, in group 'main', status: 'MONITOR'
     waiting for My-Thread@101c
      at com.acme.persistence.TransactionalSystemImpl.executeImpl(TransactionalSystemImpl.java:134)
        .
        .
        .
      at com.acme.ui.components.MyTextAreaComponent$MyDocumentListener.insertUpdate(MyTextAreaComponent.java:916)
      at javax.swing.text.AbstractDocument.fireInsertUpdate(Unknown Source:-1)
      at javax.swing.text.AbstractDocument.handleInsertString(Unknown Source:-1)
      at javax.swing.text.AbstractDocument$DefaultFilterBypass.replace(Unknown Source:-1)
      at javax.swing.text.DocumentFilter.replace(Unknown Source:-1)
      at com.acme.ui.components.FilteredDocument$InputDocumentFilter.replace(FilteredDocument.java:204)
      at javax.swing.text.AbstractDocument.replace(Unknown Source:-1)
      at javax.swing.text.JTextComponent.replaceSelection(Unknown Source:-1)
      at javax.swing.text.DefaultEditorKit$DefaultKeyTypedAction.actionPerformed(Unknown Source:-1)
      at javax.swing.SwingUtilities.notifyAction(Unknown Source:-1)
      at javax.swing.JComponent.processKeyBinding(Unknown Source:-1)
      at javax.swing.JComponent.processKeyBindings(Unknown Source:-1)
      at javax.swing.JComponent.processKeyEvent(Unknown Source:-1)
      at java.awt.Component.processEvent(Unknown Source:-1)
      at java.awt.Container.processEvent(Unknown Source:-1)
      at java.awt.Component.dispatchEventImpl(Unknown Source:-1)
      at java.awt.Container.dispatchEventImpl(Unknown Source:-1)
      at java.awt.Component.dispatchEvent(Unknown Source:-1)
      at java.awt.KeyboardFocusManager.redispatchEvent(Unknown Source:-1)
      at java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(Unknown Source:-1)
      at java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(Unknown Source:-1)
      at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(Unknown Source:-1)
      at java.awt.DefaultKeyboardFocusManager.dispatchEvent(Unknown Source:-1)
      at java.awt.Component.dispatchEventImpl(Unknown Source:-1)
      at java.awt.Container.dispatchEventImpl(Unknown Source:-1)
      at java.awt.Window.dispatchEventImpl(Unknown Source:-1)
      at java.awt.Component.dispatchEvent(Unknown Source:-1)
      at java.awt.EventQueue.dispatchEvent(Unknown Source:-1)
      at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source:-1)
      at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source:-1)
      at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source:-1)
      at java.awt.EventDispatchThread.pumpEvents(Unknown Source:-1)
      at java.awt.EventDispatchThread.pumpEvents(Unknown Source:-1)
      at java.awt.EventDispatchThread.run(Unknown Source:-1)

这是TransactionalSystemImpl.executeImpl方法:
private synchronized Object executeImpl(Transaction xact, boolean commit) {
    final Object result;

    try {
        if (commit) { // this is line 134
            clock.latch();
            synchronized(pendingEntries) {
                if (xactLatchCount > 0) {
                    pendingEntries.add(xact);
                } else {
                    xactLog.write(new TransactionEntry(xact, clock.time()));
                }
            }
        }

        final TransactionExecutor executor = transactionExecutorFactory.create(
                xact.getClass().getSimpleName()
        );

        if (executor == null) {
            throw new IllegalStateException("Failed to create transaction executor for transaction: " + xact.getClass().getName());
        }

        result = executor.execute(xact);

    } finally {
        if (commit) clock.unlatch();
    }

    return result;
}

有谁知道这里发生了什么或如何解决?

最佳答案

在我认识的Swing开发人员中,众所周知invokeAndWait是有问题的,但这也许不像我想象的那样广为人知。我似乎回想起在文档中看到关于正确使用invokeAndWait的困难的严厉警告,但是我很难找到任何东西。我在当前的官方文档中找不到任何内容。我唯一能找到的是Swing Tutorial from 2005的旧版本中的这一行:(网络存档)



不幸的是,该行似乎已从当前的Swing教程中消失。即使这样,还是轻描淡写。我本来希望这样说,“如果您使用invokeAndWait,那么调用invokeAndWait的线程一定不能持有其他线程在调用发生时可能需要的任何锁。”通常,很难知道在任何给定时间内其他线程可能需要什么锁,最安全的策略可能是确保调用invokeAndWait 的线程在所有上都不持有任何锁。

(这很难做到,这就是为什么我在上面说invokeAndWait是有问题的。我还知道JavaFX的设计者(本质上是Swing的替代品)在javafx.application.Platform类中定义了一种称为runLater的方法,该方法在功能上等效于invokeLater但他们故意省略了invokeAndWait的等效方法,因为很难正确使用。)

从第一原理中得出的理由很简单。考虑一个与OP描述的系统类似的系统,它具有两个线程:MyThread和Event Dispatch Thread(EDT)。 MyThread锁定对象L,然后调用invokeAndWait。这将发布事件E1,并等待EDT处理它。假设E1的处理程序需要锁定L。当EDT处理事件E1时,它将尝试对L进行锁定。该锁已由MyThread持有,它直到EDT处理E1才会放弃它,但是该处理被阻止了通过MyThread。因此,我们陷入僵局。

这是这种情况的变体。假设我们确保处理E1不需要锁定L。这样安全吗?否。如果在MyThread调用invokeAndWait之前,将事件E0发布到事件队列中,并且E0的处理程序需要锁定L,则仍然会发生问题。像以前一样,MyThread持有L的锁定,因此将阻止E0的处理。 E1在事件队列中位于E0后面,因此E1的处理也被阻止。由于MyThread正在等待E1的处理,并且被E0阻塞,而E0又被阻塞,等待MyThread放弃对L的锁定,因此我们再次遇到了死锁。

这听起来与OP应用程序中的操作非常相似。根据OP对this answer的评论,



我们没有完整的图片,但这可能足以继续下去。从MyThread调用renderOnEDT,当它在invokeAndWait中被阻止时,该线程将锁定某个内容。它正在等待EDT处理事件,但是我们可以看到EDT在MyThread持有的东西上被阻止了。我们不能确切地说出它是哪个对象,但这没关系-EDT显然在MyThread持有的锁上被阻塞,而MyThread显然在等待EDT处理事件:因此,死锁。

还要注意,我们可以相当肯定地确定EDT当前未处理invokeAndWait发布的事件(类似于上述情况中的E1)。如果是这样,则每次都会发生死锁。根据OP对this answer的评论,它似乎仅在某些情况下发生,当用户快速键入时。因此,我敢打赌,EDT当前正在处理的事件是一个按键,在MyThread锁定之后但在MyThread调用invokeAndWait将E1发布到事件队列之前,该事件恰好发布到了事件队列中,因此类似于E0在上述情况下。

到目前为止,这可能主要是问题的重述,它与其他答案以及OP对这些答案的评论汇总在一起。在继续讨论解决方案之前,以下是我对OP应用程序所做的一些假设:

  • 它是多线程的,因此必须同步各种对象才能正常工作。这包括来自Swing事件处理程序的调用,该事件处理程序大概基于用户交互来更新某些模型,并且该模型还由工作线程(例如MyThread)处理。因此,他们必须正确锁定此类对象。删除同步肯定会避免死锁,但是由于不同步的并发访问会破坏数据结构,因此还会出现其他错误。
  • 该应用程序不一定在EDT上执行长时间运行的操作。这是GUI应用程序的一个典型问题,但似乎在这里没有发生。我假设该应用程序在大多数情况下都可以正常工作,其中在EDT上处理的事件会获取一个锁,更新一些内容,然后释放该锁。当由于锁的持有人在EDT上死锁而无法获得锁时,就会发生问题。
  • 不能将invokeAndWait更改为invokeLater。 OP表示这样做会导致其他问题。这并不奇怪,因为更改会导致执行以不同的顺序进行,因此会产生不同的结果。我认为它们是 Not Acceptable 。

  • 如果我们无法删除锁,也无法更改为invokeLater,那么我们就可以安全地调用invokeAndWait了。 “安全”是指在调用之前放弃锁。考虑到OP应用程序的组织,这可能很难做到,但是我认为这是唯一的方法。

    让我们看看MyThread在做什么。这被大大简化了,因为可能在堆栈上有很多中间方法调用,但是从根本上来说是这样的:
    synchronized (someObject) {
        // code block 1
        SwingUtilities.invokeAndWait(handler);
        // code block 2
    }
    

    当某个事件在处理程序前面的队列中潜行时,就会发生问题,并且该事件的处理需要锁定someObject。我们如何避免这个问题?您不能在synchronized块内放弃Java的内置监视器锁之一,因此必须关闭该块,进行调用,然后再次打开它:
    synchronized (someObject) {
        // code block 1
    }
    
    SwingUtilities.invokeAndWait(handler);
    
    synchronized (someObject) {
        // code block 2
    }
    

    如果对someObject的锁定距离对invokeAndWait的调用的调用栈相当远,这可能会很困难,但我认为这样做是不可避免的。

    还有其他陷阱。如果代码块2依赖于代码块1加载的某个状态,则在代码块2再次获得锁定时,该状态可能已过时。这意味着代码块2必须从同步对象中重新加载任何状态。它不能基于代码块1的结果进行任何假设,因为这些结果可能已过时。

    这是另一个问题。假设由invokeAndWait运行的处理程序需要从共享库中加载某些状态,例如,
    synchronized (someObject) {
        // code block 1
        SwingUtilities.invokeAndWait(handler(state1, state2));
        // code block 2
    }
    

    您不能仅将invokeAndWait调用从同步块(synchronized block)中迁移出来,因为这将需要进行状态1和状态2的非同步访问。相反,您要做的是在锁定内将此状态加载到局部变量中,然后在释放锁定后使用这些局部变量进行调用。就像是:
    int localState1;
    String localState2;
    synchronized (someObject) {
        // code block 1
        localState1 = state1;
        localState2 = state2;
    }
    
    SwingUtilities.invokeAndWait(handler(localState1, localState2));
    
    synchronized (someObject) {
        // code block 2
    }
    

    释放锁定后进行 call 的技术称为开放 call 技术。请参见Doug Lea,《 Java中的并发编程》(第二版),第2.4.1.3节。 Goetz等人也对该技术进行了很好的讨论。等,《 Java Concurrency in Practice》,第10.1.4节。实际上,第10.1节中的所有内容都相当彻底地涵盖了僵局。我强烈推荐。

    总而言之,我相信使用我上面描述的技术或引用的书中的技术,可以正确,安全地解决此死锁问题。但是,我确信这将需要大量的仔分割析和困难的重组。不过,我没有其他选择。

    (最后,我应该说,尽管我是Oracle的雇员,但这绝不是Oracle的正式声明。)

    更新

    我想到了更多可能有助于解决问题的重构方法。让我们重新考虑代码的原始架构:
    synchronized (someObject) {
        // code block 1
        SwingUtilities.invokeAndWait(handler);
        // code block 2
    }
    

    这将按顺序执行代码块1,处理程序和代码块2。如果我们将invokeAndWait调用更改为invokeLater,则该处理程序将在代码块2之后执行。人们可以很容易地看到这对于应用程序是一个问题。相反,我们如何将代码块2移到invokeAndWait中,以便它以正确的顺序执行,但仍在事件线程上执行?
    synchronized (someObject) {
        // code block 1
    }
    
    SwingUtilities.invokeAndWait(Runnable {
        synchronized (someObject) {
            handler();
            // code block 2
        }
    });
    

    这是另一种方法。我不完全知道传递给invokeAndWait的处理程序打算做什么。但是,可能需要使用invokeAndWait的原因之一是它从GUI中读取了一些信息,然后使用它来更新共享状态。这必须在EDT上,因为它与GUI对象进行交互,并且invokeLater不能使用,因为它以错误的顺序发生。这建议在执行其他处理之前调用invokeAndWait,以便将信息从GUI中读取到临时区域中,然后使用该临时区域执行连续处理:
    TempState tempState;
    SwingUtilities.invokeAndWait(Runnable() {
        synchronized (someObject) {
            handler();
            tempState.update();
        }
    );
    
    synchronized (someObject) {
        // code block 1
        // instead of invokeAndWait, use tempState from above
        // code block 2
    }
    

    08-08 00:25