简体   繁体   English

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

[英]JavaFX Application Thread slows and then freezes

I have a multi-threaded JavaFX application. 我有一个多线程JavaFX应用程序。 I have a bunch of background threads that are calling this method to update the main UI from the application thread. 我有一堆后台线程正在调用此方法来从应用程序线程更新主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);

}

At first, this method works fine. 首先,这种方法很好。 But a few seconds into the program, it begins to slow, and a few more seconds later the whole UI freezes and stops responding to user input (but it doesn't trigger the Windows "This program is not responding" dialogue). 但是进入程序几秒钟后,它开始变慢,几秒钟之后整个UI冻结并停止响应用户输入(但它不会触发Windows“此程序没有响应”对话框)。 However, looking at application log files and my IDE console, the background threads are still executing and seem to be doing just fine. 但是,查看应用程序日志文件和我的IDE控制台,后台线程仍在执行,似乎做得很好。

Is there some limit as to the number of Platform.runLater() requests that can be queued up? 可以排队的Platform.runLater()请求的数量有一些限制吗? Or is there some way I might have a memory leak that is killing the main application thread but isn't doing anything to the background threads? 或者是否有某种方式我可能有一个内存泄漏,正在杀死主应用程序线程,但没有对后台线程做任何事情? I am still very new to multi-threaded programming, so I don't know what to think here. 我对多线程编程还很陌生,所以我不知道在这里想些什么。

I also know that JavaFX has another concurrency tool called Services, but I can't find any explanation as to when and why I should be using them instead of Platform.runLater() . 我也知道JavaFX有另一个名为Services的并发工具,但我找不到任何关于何时以及为什么我应该使用它们而不是Platform.runLater() I have written other JavaFX apps and never had any issues using Platform.runLater() . 我编写了其他JavaFX应用程序,使用Platform.runLater()从未遇到任何问题。

Edit: 编辑:

Thanks a lot to the two answers below. 非常感谢下面的两个答案。 The problem was nothing with JavaFX or threads per se but just plain bad code. 问题与JavaFX或线程本身无关,而只是简单的坏代码。 Here is the fixed code, for completion sake: 这是固定代码,为了完成起见:

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);
}

By concatenating strings in a loop using the += operator, you're performing an enormous amount of work. 通过使用+=运算符在循环中连接字符串,您正在执行大量工作。 If your buffer has 199 lines, doing successive concatenation will copy the first line nearly 10,000 times. 如果您的缓冲区有199行,则执行连续级联将第一行复制近10,000次。 (Though I can't quite figure out what the i >= 200 condition is doing. In addition, you're taking a text buffer of up to 200 lines and splitting it into separate strings. This involves a lot of copying. (虽然我无法弄清楚i >= 200条件正在做什么。此外,你正在使用最多200行的文本缓冲区并将其拆分为单独的字符串。这涉及大量复制。

The problem is that you're doing all this work on the event dispatch thread, which blocks up processing of the user interface. 问题是你在事件调度线程上做了所有这些工作,它阻塞了用户界面的处理。 I suspect what's happening is that this work is taking a noticeable amount of time, and the UI appears to freeze while this task is being performed. 我怀疑正在发生的事情是这项工作需要花费大量时间,并且在执行此任务时UI似乎会冻结。

I'd suggest moving the logic of updating the log's lines outside of the event handling, and have the thread that is appending the message do the work of updating the text for the buffer. 我建议在事件处理之外移动更新日志行的逻辑,并让附加消息的线程执行更新缓冲区文本的工作。 Then, call runLater() on a task that simply sets the text of the outputLog. 然后,在一个简单设置outputLog文本的任务上调用runLater()。 This avoids excessive string processing happening on the event thread. 这可以避免在事件线程上发生过多的字符串处理。

If you're going to be doing a lot of string concatenation, use a StringBuilder to append successive lines of text and then call toString() once you're all done. 如果您要进行大量的字符串连接,请使用StringBuilder追加连续的文本行,然后在完成所有操作后调用toString() This avoids redundant copying. 这可以避免冗余复制。

EDIT 编辑

Missed this earlier. 错过了这个。 The original code splits on "\\r\\n" but concatenates the lines together again using 原始代码在“\\ r \\ n”上拆分,但使用再次将这些行连接在一起

newText += lines[i];

which does not restore the line breaks. 恢复换行符。 As lines get added they end up appending to the "same" line, so the text just gets longer and longer even though it's only one line long. 随着行的添加,它们最终会附加到“相同”行,因此即使文本只有一行,文本也会越来越长。 The N-squared appending behavior might not be entering into the problem. N平方附加行为可能不会进入问题。 Instead, the output log (I assume it's a text node of some sort) has to run its line-breaking algorithm on ever-larger pieces of text. 相反,输出日志(我假设它是某种文本节点)必须在更大的文本片段上运行其换行算法。 That's probably why things slow down progressively. 这可能是事情逐渐减缓的原因。

In any case, fixing the appending problem, fixing the line-rotation logic, and moving processing out of the event loop should help. 在任何情况下,修复附加问题,修复行旋转逻辑以及将处理移出事件循环都应该有所帮助。

A couple of suggestions. 一些建议。

First, and this is just a general Java thing, it's a pretty bad idea to build a String by concatenation in a loop like this. 首先,这只是一个普通的Java事情,通过像这样的循环中的连接来构建String是一个非常糟糕的主意。 In many cases a modern compiler will fix this for you, but your if clause in there makes it harder to do so and less likely you will get this optimization automatically. 在许多情况下,现代编译器会为您修复此问题,但是您的if子句会使其更难以实现,并且您不太可能自动获得此优化。 You should make the following modifications: 您应该进行以下修改:

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

The second point is a JavaFX concurrency issue. 第二点是JavaFX并发问题。 You're not really saving anything by creating your thread here, since you simply create the thread, then schedule it to run on the FX Application Thread by passing it directly to Platform.runLater(...). 你不是通过在这里创建线程来保存任何东西,因为你只需创建线程,然后通过直接将它传递给Platform.runLater(...)来安排它在FX Application Thread上运行。 So the entire run() method is just being executed on the FX Application Thread anyway. 所以整个run()方法只是在FX Application Thread上执行。 (There are no background threads in your code!) (代码中没有后台线程!)

There are two rules to abide by: 1. Only update "live" nodes on the FX Application Thread 2. Don't perform any long-running tasks on that thread 遵守两条规则:1。仅更新FX应用程序线程2上的“实时”节点。不要在该线程上执行任何长时间运行的任务

The Task class in javafx.concurrency is a Runnable (actually a Callable, which is a little more flexible) that provides some helpful lifecycle methods that are guaranteed to be run on the FX Application Thread. javafx.concurrency中的Task类是一个Runnable(实际上是一个Callable,它更灵活一点),它提供了一些有用的生命周期方法,保证在FX Application Thread上运行。 So you can use a Task to compute your new log text, return that new log text, and then just use setOnSucceeded(..) to update the UI once it's done. 因此,您可以使用Task来计算新的日志文本,返回新的日志文本,然后只需使用setOnSucceeded(..)来更新UI。 This would look like: 这看起来像:

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();

And finally, I think if you want the last 200 lines of text, your logic is wrong. 最后,我想如果你想要最后200行文本,你的逻辑是错误的。 Try 尝试

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM