栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

涉及Swing和AWT-EventQueue的无响应线程

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

涉及Swing和AWT-EventQueue的无响应线程

在我认识的Swing开发人员中,这似乎

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

如果使用

invokeAndWait
,请确保在调用发生时,调用invokeAndWait的线程不持有其他线程可能需要的任何锁。

不幸的是,该行似乎已从当前的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对这个答案的评论,

是的,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中的E0。我上面的情况。

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

  • 它是多线程的,因此必须同步各种对象才能正常工作。这包括来自Swing事件处理程序的调用,该事件处理程序大概基于用户交互来更新某些模型,并且该模型还由工作线程(例如MyThread)处理。因此,他们 必须 正确锁定此类对象。消除同步肯定会避免死锁,但是由于不同步的并发访问会破坏数据结构,因此还会出现其他错误。

  • 该应用程序不一定在EDT上执行长时间运行的操作。这是GUI应用程序的一个典型问题,但似乎并未在这里发生。我假设该应用程序在大多数情况下都能正常工作,其中在EDT上处理的事件会获取一个锁,先进行更新,然后释放该锁。当由于锁的持有人在EDT上死锁而无法获取锁时,就会发生问题。

  • 更改

    invokeAndWait
    invokeLater
    是不是一种选择。OP表示这样做会引起其他问题。这并不奇怪,因为更改会导致执行以不同的顺序进行,因此会产生不同的结果。我认为它们是不可接受的。

如果我们无法删除锁,也无法将其更改为

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

让我们看看MyThread在做什么。这被大大简化了,因为可能在堆栈上有很多中间方法调用,但是从根本上来说是这样的:

synchronized (someObject) {    // pre block 1    SwingUtilities.invokeAndWait(handler);    // pre block 2}

当某个事件潜入处理程序前面的队列中并且该事件的处理需要lock时,就会发生问题

someObject
。我们如何避免这个问题?您不能在一个
synchronized
块内放弃Java的内置监视器锁之一,因此必须关闭该块,进行调用,然后再次打开它:

synchronized (someObject) {    // pre block 1}SwingUtilities.invokeAndWait(handler);synchronized (someObject) {    // pre block 2}

如果将锁定

someObject
从调用移到调用堆栈的上方相当远,则这可能会非常困难
invokeAndWait
,但是我认为这样做是不可避免的。

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

这是另一个问题。假设正在运行的处理程序

invokeAndWait
需要从共享库中加载一些状态,例如,

synchronized (someObject) {    // pre block 1    SwingUtilities.invokeAndWait(handler(state1, state2));    // pre block 2}

您不能只是将

invokeAndWait
调用迁移到同步块之外,因为这将需要非同步访问才能获得state1和state2。相反,您要做的是在锁定内将此状态加载到局部变量中,然后在释放锁定后使用这些局部变量进行调用。就像是:

int localState1;String localState2;synchronized (someObject) {    // pre block 1    localState1 = state1;    localState2 = state2;}SwingUtilities.invokeAndWait(handler(localState1, localState2));synchronized (someObject) {    // pre block 2}

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

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

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


更新

我想到了更多可能有助于解决问题的重构方法。让我们重新考虑代码的原始架构:

synchronized (someObject) {    // pre block 1    SwingUtilities.invokeAndWait(handler);    // pre block 2}

这将按顺序执行代码块1,处理程序和代码块2。如果将

invokeAndWait
调用更改为
invokeLater
,则处理程序将在代码块2之后执行。很容易看出这对于应用程序来说是个问题。相反,我们如何将代码块2
移到
invokeAndWait
以便它以正确的顺序执行,但仍在事件线程上执行呢?

synchronized (someObject) {    // pre block 1}SwingUtilities.invokeAndWait(Runnable {    synchronized (someObject) {        handler();        // pre block 2    }});

这是另一种方法。我不确切知道传递给处理程序的

invokeAndWait
意图是什么。但是可能需要这样做的一个原因
invokeAndWait
是它从GUI中读取了一些信息,然后使用它来更新共享状态。这必须在EDT上,因为它会与GUI对象交互,并且
invokeLater
不能使用,因为它会以错误的顺序发生。这建议在进行其他处理
invokeAndWait

之前先 进行调用,以便将信息从GUI中读取到临时区域中,然后使用该临时区域执行后续处理:

TempState tempState;SwingUtilities.invokeAndWait(Runnable() {    synchronized (someObject) {        handler();        tempState.update();    });synchronized (someObject) {    // pre block 1    // instead of invokeAndWait, use tempState from above    // pre block 2}


转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/430870.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号