当前位置: 首页 > 面试题库 >

SwingWorker,done()在process()调用完成之前执行

楚嘉胜
2023-03-14
问题内容

我已经与SwingWorker一起工作了一段时间,并最终出现了奇怪的行为,至少对我而言。我清楚地了解,由于性能原因,一次调用中合并了多个对publish()方法的调用。这对我来说非常有意义,我怀疑SwingWorker会保留某种队列来处理所有调用。

根据教程和API,当SwingWorker结束执行时,doInBackground()正常完成,或者从外部取消了工作线程,然后调用done()方法。到目前为止,一切都很好。

但是我有一个示例(类似于教程中显示的示例),其中在执行process()方法 之后
done()执行了一些方法调用。由于这两种方法都在事件调度线程中执行,所以我希望done()所有process()调用完成后才能执行。换一种说法:

预期:

Writing...
Writing...
Stopped!

结果:

Writing...
Stopped!
Writing...

样例代码

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class Demo {

    private SwingWorker<Void, String> worker;
    private JTextArea textArea;
    private Action startAction, stopAction;

    private void createAndShowGui() {

        startAction = new AbstractAction("Start writing") {
            @Override
            public void actionPerformed(ActionEvent e) {
                Demo.this.startWriting();
                this.setEnabled(false);
                stopAction.setEnabled(true);
            }
        };

        stopAction = new AbstractAction("Stop writing") {
            @Override
            public void actionPerformed(ActionEvent e) {
                Demo.this.stopWriting();
                this.setEnabled(false);
                startAction.setEnabled(true);
            }
        };

        JPanel buttonsPanel = new JPanel();
        buttonsPanel.add(new JButton(startAction));
        buttonsPanel.add(new JButton(stopAction));

        textArea = new JTextArea(30, 50);
        JScrollPane scrollPane = new JScrollPane(textArea);

        JFrame frame = new JFrame("Test");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(scrollPane);
        frame.add(buttonsPanel, BorderLayout.SOUTH);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void startWriting() {
        stopWriting();
        worker = new SwingWorker<Void, String>() {
            @Override
            protected Void doInBackground() throws Exception {
                while(!isCancelled()) {
                    publish("Writing...\n");
                }
                return null;
            }

            @Override
            protected void process(List<String> chunks) {
                String string = chunks.get(chunks.size() - 1);
                textArea.append(string);
            }

            @Override
            protected void done() {
                textArea.append("Stopped!\n");
            }
        };
        worker.execute();
    }

    private void stopWriting() {
        if(worker != null && !worker.isCancelled()) {
            worker.cancel(true);
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Demo().createAndShowGui();
            }
        });
    }
}

问题答案:

简短答案:

发生这种情况是因为publish()不直接进行调度process,而是设置了一个计时器,该计时器将在EDT之后触发EDT中process()块的调度DELAY。因此,当取消工作程序时,仍然有一个计时器等待使用上次发布的数据来调度process()。使用计时器的原因是为了实现优化,其中可以使用多个发布的组合数据执行单个过程。

长答案:

让我们看看publish()和cancel如何相互交互,为此,让我们深入研究一些源代码。

首先是简单的部分cancel(true)

public final boolean cancel(boolean mayInterruptIfRunning) {
    return future.cancel(mayInterruptIfRunning);
}

此取消最终将调用以下代码:

boolean innerCancel(boolean mayInterruptIfRunning) {
    for (;;) {
        int s = getState();
        if (ranOrCancelled(s))
            return false;
        if (compareAndSetState(s, CANCELLED)) // <-----
            break;
    }
    if (mayInterruptIfRunning) {
        Thread r = runner;
        if (r != null)
            r.interrupt(); // <-----
    }
    releaseShared(0);
    done(); // <-----
    return true;
}

SwingWorker状态设置为CANCELLED,线程被中断并被done()调用,但这不是SwingWorker的完成,而是futuredone(),它在SwingWorker构造函数中实例化变量时指定:

future = new FutureTask<T>(callable) {
    @Override
    protected void done() {
        doneEDT();  // <-----
        setState(StateValue.DONE);
    }
};

doneEDT()代码是:

private void doneEDT() {
    Runnable doDone =
        new Runnable() {
            public void run() {
                done(); // <-----
            }
        };
    if (SwingUtilities.isEventDispatchThread()) {
        doDone.run(); // <-----
    } else {
        doSubmit.add(doDone);
    }
}

done()如果我们在EDT中,则直接调用SwingWorkers
,这就是我们的情况。此时,SwingWorker应该停止,不再publish()调用,这很容易通过以下修改进行演示:

while(!isCancelled()) {
    textArea.append("Calling publish\n");
    publish("Writing...\n");
}

但是,我们仍然从process()收到“正在写…”消息。因此,让我们看看如何调用process()。的源代码publish(...)

protected final void publish(V... chunks) {
    synchronized (this) {
        if (doProcess == null) {
            doProcess = new AccumulativeRunnable<V>() {
                @Override
                public void run(List<V> args) {
                    process(args); // <-----
                }
                @Override
                protected void submit() {
                    doSubmit.add(this); // <-----
                }
            };
        }
    }
    doProcess.add(chunks);  // <-----
}

我们看到run()Runnable
doProcess的最终是谁调用process(args),但是这段代码只是doProcess.add(chunks)不调用,周围也doProcess.run()有一个doSubmit。让我们来看看doProcess.add(chunks)

public final synchronized void add(T... args) {
    boolean isSubmitted = true;
    if (arguments == null) {
        isSubmitted = false;
        arguments = new ArrayList<T>();
    }
    Collections.addAll(arguments, args); // <-----
    if (!isSubmitted) { //This is what will make that for multiple publishes only one process is executed
        submit(); // <-----
    }
}

因此,publish()实际要做的是将这些块添加到一些内部ArrayList中arguments并调用submit()。刚才我们看到的是只提交调用doSubmit.add(this),这是这同一个add方法,因为这两个doProcessdoSubmit扩大AccumulativeRunnable<V>,但是这一次VRunnable不是StringdoProcess。因此,可调用的块是可运行的process(args)。但是,该submit()调用是在类中定义的完全不同的方法doSubmit

private static class DoSubmitAccumulativeRunnable
     extends AccumulativeRunnable<Runnable> implements ActionListener {
    private final static int DELAY = (int) (1000 / 30);
    @Override
    protected void run(List<Runnable> args) {
        for (Runnable runnable : args) {
            runnable.run();
        }
    }
    @Override
    protected void submit() {
        Timer timer = new Timer(DELAY, this); // <-----
        timer.setRepeats(false);
        timer.start();
    }
    public void actionPerformed(ActionEvent event) {
        run(); // <-----
    }
}

它创建一个计时器,该计时器将actionPerformed在数DELAY毫秒后触发一次代码。一旦事件被触发的代码将在EDT被排队,这将调用内部run(),其结束调用run(flush())doProcess并且因此执行process(chunk),其中块是的刷新的数据arguments的ArrayList。我跳过了一些细节,“运行”调用链如下所示:

  • doSubmit.run()
  • doSubmit.run(flush())//实际上是一个可运行的循环,但只有一个(*)
  • doProcess.run()
  • doProcess.run(flush())
  • 处理(块)

(*)布尔值isSubmitedflush()(将重置此布尔值)使得它的发布附加调用不会在doSubmit.run(flush())中添加要调用的doProcess可运行对象,但是不会忽略它们的数据。因此,对于一个计时器的生命周期内调用的任意数量的发布执行单个进程。

总而言之,在DELAY
之后publish("Writing...")将调用调度到process(chunk)EDT中。这解释了为什么即使在我们取消线程并且不再进行任何发布之后,仍然会出现一个进程执行,因为在我们取消工作人员的那一刻(很有可能),已经安排了一个计划之后的计时器。
__process()``done()

为什么使用此Timer而不是仅在带有EDT的EDT中调度process()invokeLater(doProcess)?要实现文档中说明的性能优化:

由于该处理方法是在事件调度线程上异步调用的,因此在执行该处理方法之前,可能会对发布方法进行多次调用。为了性能起见,所有这些调用都被合并为一个带有级联参数的调用。例如:

 publish("1");
 publish("2", "3");
 publish("4", "5", "6");

might result in:
 process("1", "2", "3", "4", "5", "6")

现在我们知道这是可行的,因为在DELAY间隔内发生的所有发布都将它们添加args到我们看到的内部变量中,arguments并且process(chunk)将一次性处理所有数据。

这是一个错误吗? 解决方法?

很难说这是否是一个错误,处理后台线程已发布的数据可能很有意义,因为该工作实际上已经完成,并且您可能有兴趣用尽可能多的信息来更新GUI
process()例如,如果这样做的话)。然后,如果done()需要处理所有数据和/或在done()创建数据/
GUI不一致后调用process(),可能就没有意义了。

如果您不想在done()之后执行任何新的process(),则有一个明显的解决方法,只需检查该process方法中的worker是否也被取消!

@Override
protected void process(List<String> chunks) {
    if (isCancelled()) return;
    String string = chunks.get(chunks.size() - 1);
    textArea.append(string);
}

要使done()在最后一个process()之后执行比较棘手,例如done可以只使用一个计时器,该计时器将在>
DELAY之后安排实际的done()工作。尽管我不认为这是常见的情况,因为如果您取消了它,那么当我们知道实际上正在取消所有将来的执行时,再错过一个process()也不重要。



 类似资料:
  • 问题内容: 我创建了一个for循环,该循环循环了元素出现在容器中的次数。for循环从HTML捕获一些数据,并创建一个JSON url,然后将返回一个值。然后应将该值添加到适当位置的HTML中。 问题似乎是for循环在进行所有Ajax调用之前完成,因此仅将最后一个值添加到HTML。我以为可以确保readystate等于4,但是该解决方案不起作用。我还尝试将完整而不是成功用作Ajax事件。有什么见解吗

  • 我有一个用于交互式过渡的自定义动画师。还有一个,根据过渡进度设置为。效果的动画代码如下: 我通过调用它,当从它到第一个的转换开始时,它在第二个上调用。 然而,我这里有一个问题。在动画结束之前调用完成块。当我第一次运行转换(没有取消它)时,它工作得很好,但在随后的运行过程中,它就不工作了。 我也曾尝试将动画添加到我的动画师中,但也没有成功。 此外,当我取消转换时,在实际动画结束之前调用完成块(在这种

  • 问题内容: 所述javax.servlet.Filter的对象可以用于认证使用二者(其中过滤器需要赶上请求需要做任何servlet工作需要之前)和XSLT转换(其中的servlet需要是完全完成生成的内容)。它什么时候真正执行? 我知道这是依赖于实现的(在Web容器上),但这似乎是所有问题都需要解决的问题。 也许在某个地方为每个向Web容器注册的过滤器设置了一个配置选项? 额外: 另外,什么决定了

  • 您的Google Maps代码结构如下所示: 回调? JavaScriptpromise? 您能想到的其他方法吗? 同样,这里所建议的信号量是一种可行的方法吗?

  • javax。servlet。Filter对象既可以用于身份验证(在需要完成任何servlet工作之前,过滤器需要捕获请求),也可以用于XSLT转换(servlet需要完全生成内容)。什么时候执行? 我知道这取决于实现(取决于web容器),但这似乎是所有人都需要解决的问题。 也许在web容器的每个过滤器注册的地方都设置了一个配置选项? 其他: 此外,什么控制过滤器的执行顺序?为什么FooFilter

  • 问题内容: 我正在使用 dialogflow-fulfillment- nodejs 客户端开发Dialogflow webhook,以查找城市的温度。在使用服务获取城市温度时,一切正常,还会生成正确的响应,但是即使调用正确的方法,也不会将响应发送给用户。 这是Dialogflow GitHub存储库中的问题 码 控制台日志 当注释的代码运行时 当未注释的代码运行时 NodeJS Dialogfl