Pau*_*ers 12 java concurrency swing multithreading deadlock
我有一个没有响应的应用程序,似乎陷入僵局或类似僵局.请参阅下面的两个主题.请注意,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)
Run Code Online (Sandbox Code Playgroud)
这是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;
}
Run Code Online (Sandbox Code Playgroud)
有谁知道这里发生了什么或如何解决它?
寻找可信和/或官方来源的答案.
Event Dispatch Thread and EventQueue
Swing事件处理代码在称为事件调度线程(EDT)的特殊线程上运行.大多数调用Swing方法的代码也在这个线程上运行.这是必要的,因为大多数Swing对象方法都不是线程安全的.所有与GUI相关的任务,都应该对GUI进行任何更新,而绘制过程必须在EDT上进行,这涉及将请求包装在一个事件中并将其处理到EventQueue.然后,事件将从一个接一个的队列中按顺序分派,即FIRST IN FIRST OUT.也就是说,如果Event A被排队到EventQueue之前,Event B那么事件之前B将不会发送事件A.
SwingUtilities class有两个有用的功能来帮助GUI渲染任务:
invokeLater(Runnable):doRun.run()在AWT事件派发线程(EDT)上异步执行的原因.如上所述,这将在所有待处理的AWT事件被处理之后发生.invokeAndWait(Runnable):它具有invokeLater与之相同的功能,但它与invokeLater以下事实不同:
invokeAndWait 等待它给EDT的任务,在返回之前完成.WAIT通过同步锁定来通过发送到状态来调用继续执行它的线程.源代码有证据:
public static void invokeAndWait(Runnable runnable)
throws InterruptedException, InvocationTargetException {
if (EventQueue.isDispatchThread())
throw new Error("Cannot call invokeAndWait from the event dispatcher thread");
class AWTInvocationLock {}
Object lock = new AWTInvocationLock();
InvocationEvent event = new InvocationEvent(Toolkit.getDefaultToolkit(),
runnable, lock,
true);
synchronized (lock) { //<<---- locking
Toolkit.getEventQueue().postEvent(event);
while (!event.isDispatched()) { //<---- checking if the event is dispatched
lock.wait(); //<---- if not tell the current invoking thread to wait
}
}
Throwable eventThrowable = event.getThrowable();
if (eventThrowable != null) {
throw new InvocationTargetException(eventThrowable);
}
}
Run Code Online (Sandbox Code Playgroud)
这解释了这个问题:
My-Thread刚刚调用了java.awt.EventQueue.invokeAndWait().所以AWT-EventQueue-0阻止My-Thread(我相信).
为了解释您可能遇到的死锁情况,让我们看一个例子:
class ExampleClass
{
public synchronized void renderInEDT(final Thread t)
{
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
System.out.println("Executinf invokeWait's Runnable ");
System.out.println("invokeWait invoking Thread's state: "+t.getState());
doOtherJob();
}
});
} catch (InterruptedException ex) {
Logger.getLogger(SwingUtilitiesTest.class.getName()).log(Level.SEVERE, null, ex);
} catch (InvocationTargetException ex) {
Logger.getLogger(SwingUtilitiesTest.class.getName()).log(Level.SEVERE, null, ex);
}
}
public synchronized void renderInEDT2(final Thread t)
{
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
System.out.println("Executing invokeLater's Runnable ");
System.out.println("invokeLater's invoking Thread's state: "+t.getState());
doOtherJob();
}
});
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
Logger.getLogger(ExampleClass.class.getName()).log(Level.SEVERE, null, ex);
}
}
public synchronized void doOtherJob()
{
System.out.println("Executing a job inside EDT");
}
}
Run Code Online (Sandbox Code Playgroud)
如您所见,我已声明三个同步功能:
renderInEDT(final Thread t):在EDT中执行Runnable任务SwingUtilities.invokeAndWaitrenderInEDT2(final Thread t):在EDT中执行Runnable任务 SwingUtilities.invokeLaterdoOtherJob():这个函数由上面两个函数的RunnableS run()方法调用. 传递调用线程的引用以检查每个SwingUtilities函数调用的状态.现在,如果我们renderInEDT()在一个实例exmpleClass上调用ExampleClass:这Thread t是在类上下文中声明的:
t = new Thread("TestThread"){
@Override
public void run() {
exmpleClass.renderInEDT(t);
}
};
t.start();
Run Code Online (Sandbox Code Playgroud)
输出将是:
Executing invokeWait's Runnable
invokeWait invoking Thread's state: WAITING
Run Code Online (Sandbox Code Playgroud)
该doOtherJob()方法永远不会在发布的EDT中执行,SwingUtilities.invokeAndWait因为出现死锁情况.由于renderInEDT()同步并在线程内执行t,EDT将需要等待执行,doOtherJob()直到第一次调用线程完成执行该renderInEDT(final Thread t)方法,如下所述official tutorial source of synchronized method:
对同一对象的两个同步方法的调用不可能进行交错.当一个线程正在为对象执行同步方法时,所有其他线程调用同一对象的同步方法(暂停执行)直到第一个线程完成对象.
因此,EDT正在等待t线程完成(和暂停)执行,但线程t实际上被阻塞并通过上述SwingUtilities.invokeAndWait方法发送到等待状态 ,因此它无法完成它的执行:EDT的Bth和t线程正在等待彼此执行.
让我们看一下上面的例子:如果我们使用事件任务发布,SwingUtilities.invokeLater因为如果我们从一个线程执行实例renderInEDT2()上的函数将会很明显exampleClass:
t = new Thread("TestThread"){
@Override
public void run() {
exmpleClass.renderInEDT2(t);
}
};
t.start();
Run Code Online (Sandbox Code Playgroud)
这次你会看到,函数调用继续正常产生以下输出:
Executing invokeLater's Runnable
invokeLater's invoking Thread's state: TIMED_WAITING
Executing a job inside EDT
Run Code Online (Sandbox Code Playgroud)
doOtherJob()一旦第一个调用线程renderInEDT2()完成执行,这个时间就由EDT 执行:强调我已经让线程进入休眠状态(3s)来检查执行时间,因此它显示了状态TIMED_WAITING.
这就是解释你的第二个问题的原因:正如你所说的那样,你的一条评论中也有例外:
enderOnEDT在调用堆栈中以某种方式同步,即同步的com.acme.persistence.TransactionalSystemImpl.executeImpl方法.而renderOnEDT正在等待进入同样的方法.所以,这就是它看起来像死锁的根源.现在我必须弄清楚如何解决它.
但是,
SwingUtilities.invokeAndWait(Runnable)特别是当我们想要阻止或等待线程并询问用户是否应该继续使用JOptionPane/JDialogue/JFileChooser等时使用.EventQueue,请使用SwingUtilities.invokeLater(Runnable).Thread.sleep(time)用于演示目的,请不要做任何这样的事情,这可能会阻止EDT提前一段时间,即使它很少,否则你的Swing会被冻结并迫使你杀掉它.Runnable的invokeAndWait和 invokeLater功能.此时,您自己应该能够弄清楚并解决您的问题,因为您没有向我们提供足够的详细信息.我认为同时使用像同步功能可以发布您的GUI渲染任务事件队列enderOnEDT为您在您的评论说的话,我看不出有任何理由调用另一个同步功能,从它的Runnable.而是Runnable直接将渲染功能放入其中.这是我解释事件队列和EDT机制的唯一目的.
参考:
The Event Dispatch ThreadClass EventQueueInitial ThreadsSwingUtilities.invokeAndWait(Runnable doRun) documentation我认识的 Swing 开发人员似乎都知道这invokeAndWait是有问题的,但也许这并不像我想象的那样广为人知。我似乎记得在文档中看到过关于invokeAndWait正确使用困难的严厉警告,但我很难找到任何东西。我在当前的官方文档中找不到任何内容。我唯一能找到的是2005 年旧版Swing 教程中的这一行:(网络存档)
如果使用
invokeAndWait,请确保调用 invokeAndWait 的线程在调用发生时不持有其他线程可能需要的任何锁。
不幸的是,这条线似乎从当前的 Swing 教程中消失了。即使这是一种轻描淡写的说法。我更希望它说“如果您使用invokeAndWait,则调用的线程invokeAndWait 不得持有其他线程在调用发生时可能需要的任何锁。” 一般来说,很难知道在任何给定时间内其他线程可能需要什么锁,最安全的策略可能是确保线程调用invokeAndWait 根本不持有任何锁。
(这很难做到,这就是为什么我在上面说这invokeAndWait是有问题的。我也知道 JavaFX 的设计者——本质上是 Swing 的替代品——在javafx.application.Platform类中定义了一个调用的方法runLater,该方法在功能上是等效的to invokeLater。但他们故意省略了一个等效的方法,invokeAndWait因为它很难正确使用。)
从第一性原理推导出来的原因相当简单。考虑一个类似于 OP 描述的系统,它有两个线程:MyThread 和事件调度线程 (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对此答案的评论,
是的,renderOnEDT 在调用堆栈中以某种方式同步,即同步的 com.acme.persistence.TransactionalSystemImpl.executeImpl 方法。而 renderOnEDT 正在等待进入相同的方法。所以,这就是它看起来像的僵局的根源。现在我必须弄清楚如何修复它。
我们没有完整的图片,但这可能足以继续下去。renderOnEDT正在从 MyThread 调用,当它在invokeAndWait. 它正在等待 EDT 处理一个事件,但我们可以看到 EDT 被 MyThread 持有的某些东西阻塞了。我们不能确切地说出这是哪个对象,但这并不重要——EDT 显然被 MyThread 持有的锁阻塞了,而且 MyThread 显然正在等待 EDT 处理一个事件:因此,死锁.
还要注意,我们可以相当确定 EDT 当前没有处理由invokeAndWait(类似于我上面场景中的 E1)发布的事件。如果是这样,每次都会发生死锁。根据 OP对此答案的评论,当用户快速输入时,它似乎只发生在某些时候。所以我敢打赌 EDT 当前正在处理的事件是在 MyThread 获取锁定之后碰巧发布到事件队列的击键,但在 MyThread 调用invokeAndWait将 E1 发布到事件队列之前,因此它类似于 E0我上面的场景。
到目前为止,这可能主要是对问题的回顾,从其他答案和 OP 对这些答案的评论中拼凑而成。在我们继续讨论解决方案之前,以下是我对 OP 应用程序所做的一些假设:
它是多线程的,因此必须同步各种对象才能正常工作。这包括来自 Swing 事件处理程序的调用,它可能会根据用户交互更新某些模型,并且该模型也由工作线程(例如 MyThread)处理。因此,他们必须正确锁定此类对象。删除同步肯定会避免死锁,但由于数据结构被非同步并发访问破坏,其他错误也会蔓延。
应用程序不一定在 EDT 上执行长时间运行的操作。这是 GUI 应用程序的典型问题,但这里似乎没有发生。我假设应用程序在大多数情况下都能正常工作,在 EDT 上处理的事件获取锁,更新某些内容,然后释放锁。当它无法获得锁时就会出现问题,因为锁的持有者在 EDT 上死锁了。
更改invokeAndWait到invokeLater是不是一种选择。OP 表示这样做会导致其他问题。这并不奇怪,因为这种变化会导致执行以不同的顺序发生,因此会产生不同的结果。我会假设他们是不可接受的。
如果我们无法移除锁,并且无法更改为invokeLater,那么我们只能invokeAndWait安全地调用。“安全”意味着在调用它之前放弃锁定。考虑到 OP 应用程序的组织,这可能很难做到,但我认为这是进行的唯一方法。
让我们看看 MyThread 在做什么。这大大简化了,因为堆栈上可能有一堆中间方法调用,但基本上是这样的:
synchronized (someObject) {
// code block 1
SwingUtilities.invokeAndWait(handler);
// code block 2
}
Run Code Online (Sandbox Code Playgroud)
当某些事件潜入处理程序前面的队列中时,就会出现问题,并且该事件的处理需要锁定someObject。我们怎样才能避免这个问题?您不能放弃一个synchronized块内的 Java 内置监视器锁之一,因此您必须关闭该块,进行调用,然后再次打开它:
synchronized (someObject) {
// code block 1
}
SwingUtilities.invokeAndWait(handler);
synchronized (someObject) {
// code block 2
}
Run Code Online (Sandbox Code Playgroud)
如果锁定在someObject调用到的调用堆栈中相当远,这可能会非常困难invokeAndWait,但我认为进行这种重构是不可避免的。
还有其他的陷阱。如果代码块 2 依赖于代码块 1 加载的某个状态,那么该状态可能会在代码块 2 再次锁定时过时。这意味着代码块 2 必须从同步对象重新加载任何状态。它不能基于代码块 1 的结果做出任何假设,因为这些结果可能已经过时。
这是另一个问题。假设正在运行的处理程序invokeAndWait需要从共享对象加载一些状态,例如,
synchronized (someObject) {
// code block 1
SwingUtilities.invokeAndWait(handler(state1, state2));
// code block 2
}
Run Code Online (Sandbox Code Playgroud)
您不能只是将invokeAndWait调用从同步块中迁移出来,因为这需要非同步访问以获取 state1 和 state2。相反,您必须做的是在锁定内将此状态加载到局部变量中,然后在释放锁定后使用这些局部变量进行调用。就像是:
int localState1;
String localState2;
synchronized (someObject) {
// code block 1
localState1 = state1;
localState2 = state2;
}
SwingUtilities.invokeAndWait(handler(localState1, localState2));
synchronized (someObject) {
// code block 2
}
Run Code Online (Sandbox Code Playgroud)
在释放锁后进行调用的技术称为开放调用技术。请参阅 Doug Lea,Java 中的并发编程(第 2 版),第 2.4.1.3 节。Goetz 等人也对这种技术进行了很好的讨论。al.,Java 并发实践,第 10.1.4 节。事实上,第 10.1 节的所有内容都相当彻底地涵盖了死锁;我强烈推荐它。
总而言之,我相信使用我上面描述的技术或引用的书籍中的技术,将正确且安全地解决这个死锁问题。但是,我确信这也需要大量仔细的分析和艰难的重组。不过,我没有看到替代方案。
(最后,我应该说,虽然我是 Oracle 的员工,但这绝不是 Oracle 的官方声明。)
更新
我想到了一些可能有助于解决问题的潜在重构。让我们重新考虑代码的原始架构:
synchronized (someObject) {
// code block 1
SwingUtilities.invokeAndWait(handler);
// code block 2
}
Run Code Online (Sandbox Code Playgroud)
这将按顺序执行代码块 1、处理程序和代码块 2。如果我们将invokeAndWait调用更改为invokeLater,则处理程序将在代码块 2 之后执行。很容易看出这对应用程序来说是一个问题。相反,我们如何将代码块 2 移动到 中,invokeAndWait以便它以正确的顺序执行,但仍然在事件线程上?
synchronized (someObject) {
// code block 1
}
SwingUtilities.invokeAndWait(Runnable {
synchronized (someObject) {
handler();
// code block 2
}
});
Run Code Online (Sandbox Code Playgroud)
这是另一种方法。我不确切知道传递给的处理程序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
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
6663 次 |
| 最近记录: |