JavaFX应用程序线程减慢然后冻结

时间:2013-12-13 22:55:49

标签: java multithreading concurrency javafx-2

我有一个多线程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);

}

首先,这种方法很好用。但是进入程序几秒钟后,它开始变慢,几秒钟之后整个UI冻结并停止响应用户输入(但它不会触发Windows“此程序没有响应”对话框)。但是,查看应用程序日志文件和我的IDE控制台,后台线程仍在执行,似乎做得很好。

对于可以排队的Platform.runLater()次请求数量是否有限制?或者是否有某种方式我可能有一个内存泄漏,正在杀死主应用程序线程,但没有对后台线程做任何事情?我仍然是多线程编程的新手,所以我不知道在这里想些什么。

我也知道JavaFX有另一个名为Services的并发工具,但是我找不到关于何时以及为什么我应该使用它们而不是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);
}

2 个答案:

答案 0 :(得分:3)

通过使用+=运算符在循环中连接字符串,您正在执行大量工作。如果您的缓冲区有199行,则执行连续级联将复制第一行 10,000 次。 (虽然我无法弄清楚i >= 200条件正在做什么。此外,你正在使用最多200行的文本缓冲区并将其拆分为单独的字符串。这涉及大量复制。 / p>

问题是你在事件调度线程上做了所有这些工作,它阻塞了用户界面的处理。我怀疑正在发生的事情是这项工作需要花费大量时间,并且在执行此任务时UI似乎会冻结。

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

如果您要进行大量字符串连接,请使用StringBuilder附加连续的文本行,然后在完成后调用toString()。这可以避免冗余复制。

修改

之前错过了这个。原始代码在“\ r \ n”上拆分,但使用

再次将这些行连接在一起
newText += lines[i];

恢复换行符。随着行的添加,它们最终会附加到“相同”行,因此即使文本只有一行,文本也会越来越长。 N平方附加行为可能不会进入问题。相反,输出日志(我假设它是某种文本节点)必须在更大的文本片段上运行其换行算法。这可能是事情逐渐减缓的原因。

在任何情况下,修复附加问题,修复行旋转逻辑以及将处理移出事件循环都应该有所帮助。

答案 1 :(得分:3)

一些建议。

首先,这只是一个普通的Java事情,在这样的循环中通过串联构建String是一个非常糟糕的主意。在许多情况下,现代编译器会为您修复此问题,但是您的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 Application Thread上运行。所以整个run()方法只是在FX Application Thread上执行。 (代码中没有后台线程!)

遵守两条规则: 1.仅更新FX应用程序线程上的“实时”节点 2.不要在该线程上执行任何长时间运行的任务

javafx.concurrency中的Task类是一个Runnable(实际上是一个Callable,它更灵活一点),它提供了一些有用的生命周期方法,保证在FX Application Thread上运行。因此,您可以使用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]);
}