JavaFX应用程序线程变慢然后冻结。

3

我有一个多线程的JavaFX应用程序。我有一堆后台线程正在调用此方法,以从应用程序线程更新主UI。

public void writeToActivityLog(String message){

    class ThreadedTask implements Runnable {

        private String message;

        public ThreadedTask(String message){
            this.message = message;
        }

        @Override
        public void run() {
            try{
                //delete older text when there are more than 200 lines and append new text

                String text = outputLog.getText();
                String[] lines = text.split("\r\n");
                String newText = "";
                for(int i = 0; i < lines.length; i++){
                    if(i >= 200 || lines.length < 200){
                        newText += lines[i];
                    }
                }
                outputLog.setText(newText);
                outputLog.appendText(message + "\r\n");

                //scroll to the bottom of the log
                outputLog.setScrollTop(Double.MIN_VALUE);

            } catch(NullPointerException e){
                e.printStackTrace();
            }
        }
    }

    ThreadedTask thread = new ThreadedTask(message);
    Platform.runLater(thread);

}

一开始,这个方法表现得很好。但是几秒钟后,程序开始变慢,再过几秒钟整个用户界面就会冻结并停止响应用户输入(但不会触发Windows的“该程序未响应”对话框)。然而,在查看应用程序日志文件和IDE控制台时,后台线程仍在执行,并且似乎做得很好。
是否存在关于可以排队的Platform.runLater()请求数量的限制?或者我可能有一个内存泄漏的方式,它正在杀死主应用程序线程,但没有做任何事情来影响后台线程?我还是多线程编程的新手,所以我不知道应该怎么想。
我也知道JavaFX还有另一种并发工具称为服务,但我找不到任何解释何时以及为什么我应该使用它们而不是Platform.runLater()。我已经编写了其他JavaFX应用程序,并且从来没有出现过任何使用Platform.runLater()的问题。
编辑:
感谢下面两个答案。问题并不是JavaFX或线程本身的问题,而只是糟糕的代码。以下是修正后的代码,供完整性:
public void writeToActivityLog(String message){
    //Create a new thread to update the UI so that it doesn't freeze due to concurrency issues
    class ThreadedTask implements Runnable {

        private String message;

        public ThreadedTask(String message){
            this.message = message;
        }

        @Override
        public void run() {
            outputLog.setText(message);

            //scroll to the bottom of the log
            outputLog.setScrollTop(Double.MIN_VALUE);
        }
    }

    String wholeMessage = new StringBuilder().append(outputLog.getText()).append(message).toString();
    //modify the text of the output log so that it is 200 lines or less
    StringBuilder newText = new StringBuilder();
    String[] lines = wholeMessage.split("\r\n");

    for (int i=Math.max(0, lines.length - 200); i<lines.length; i++) {
        newText.append(new StringBuilder().append(lines[i]).append("\r\n").toString());
    }

    ThreadedTask thread = new ThreadedTask(newText.toString());
    Platform.runLater(thread);
}

1
执行ThreadedTask的频率是多少?过于频繁调用Platform.runLater()可能会导致问题。每秒排队和执行数百个Runnables并不是一个好的建议。 - denhackl
你的任务是否执行速度比提交速度慢,导致它们在队列中“积压”,使平台难以响应图形事件? - Giulio Franco
2个回答

4

一些建议。

首先,这只是一个普遍的Java问题,在循环中使用连接构建字符串是一个非常糟糕的想法。在许多情况下,现代编译器会自动为您解决这个问题,但您的if子句使其更难以实现,并且不太可能自动获得此优化。您应该进行以下修改:

// String newText = "" ;
StringBuilder newText = new StringBuilder();
// ...
// newText += lines[i] ;
newText.append(lines[i]);
// ...
// outputLog.setText(newText);
outputLog.setText(newText.toString());

第二个问题是JavaFX并发问题。在此创建线程并没有真正节省任何东西,因为您只是创建线程,然后通过将其直接传递给Platform.runLater(...)来安排它在FX应用程序线程上运行。因此,整个run()方法实际上仍然在FX应用程序线程上执行。(您的代码中没有后台线程!)
有两个规则必须遵守: 1. 仅在FX应用程序线程上更新“活动”节点 2. 不要在该线程上执行任何长时间运行的任务
javafx.concurrency中的Task类是一个Runnable(实际上是一个更灵活的Callable),提供一些有用的生命周期方法,保证在FX应用程序线程上运行。因此,您可以使用Task计算新的日志文本,返回该新的日志文本,然后只需使用setOnSucceeded(...)一旦完成就更新UI。代码如下:
class UpdateLogTask extends Task<String> { // a Task<T> has a call() method returning a T
    private final String currentLog ;
    private final String message ;
    public UpdateLogTask(String currentLog, String message) {
      this.currentLog = currentLog ;
      this.message = message ;
    }
    @Override
    public String call() throws Exception {
      String[] lines = currentLog.split("\r\n");
      StringBuilder text = new StringBuilder();
      for (int i=0; i<lines.length; i++) {
        if (i>=200 || lines.length < 200) {
          text.append(lines[i]);
        }
      }
      text.append(message).append("\r\n");
      return text.toString() ;
    }
}
final Task<String> updateLogTask = new UpdateLogTask(outputLog.getText(), message);
updateLogTask.setOnSucceeded(new EventHandler<WorkerStateEvenet>() {
    @Override
    public void handle(WorkerStateEvent event) {
      outputLog.setText(updateLogTask.getValue());
    }
});
Thread t = new Thread(updateLogTask);
t.setDaemon(true); // will terminate if JavaFX runtime terminates
t.start();

最后,如果你想要最后200行的文本,我认为你的逻辑是错误的。请尝试

for (int i=Math.max(0, lines.length - 200); i<lines.length; i++) {
  text.append(lines[i]);
}

啊,当我在输入时Stuart几乎发了完全相同的建议...对不起造成了重复发帖。代码可能会有所帮助,所以我会把它留在这里。 - James_D
好吧,我们将从两侧对他(或她)进行攻击。 :-) - Stuart Marks
谢谢您的建议。尽可能将逻辑保持在UI线程之外是有道理的。(有后台线程,只是没有在我发布的片段中。数百个线程。) - Joshua Welker

4
通过使用+=运算符在循环中连接字符串,您正在执行大量的工作。如果您的缓冲区有199行,则进行连续连接将近乎复制第一行10,000次。(尽管我无法确定i>= 200条件的作用。此外,您将最多200行的文本缓冲区拆分为单独的字符串。这涉及到大量的复制。

问题在于您正在事件调度线程上执行所有这些工作,这会阻塞用户界面的处理。我怀疑正在发生的是,这项工作需要相当长的时间,而在执行此任务时,UI似乎会冻结。

我建议将更新日志行的逻辑移出事件处理,并让附加消息的线程执行更新缓冲区文本的工作。然后,在一个简单地设置输出日志文本的任务上调用runLater()。这避免了事件线程上发生过多的字符串处理。

如果您将要进行大量的字符串连接,请使用StringBuilder来添加连续的文本行,然后在完成所有操作后调用toString()。这避免了不必要的复制。

编辑

之前漏掉了这个。原始代码在"\r\n"上进行拆分,但使用时将行重新连接在一起。

newText += lines[i];

这段文本涉及IT技术,其中提到的问题是不能恢复换行符。随着新行的添加,它们会附加到“同一”行上,因此即使只有一行,文本也会变得越来越长。N平方的追加行为可能没有进入问题。相反,输出日志(我假设它是某种文本节点)必须对越来越大的文本块运行其断行算法。这可能就是为什么事情会逐渐变慢的原因。

无论如何,解决追加问题、修复行旋转逻辑以及将处理移出事件循环都应该有所帮助。


谢谢您的建议。我甚至没有考虑到字符串问题...我在昏迷中写了那一小段,从来没有认真思考过。是的,回想起来,我觉得它很糟糕。我想我太过于困扰于晦涩的多线程问题,而忽略了显而易见的问题。 - Joshua Welker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接