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