JavaFX - 从另一个线程更新 TextArea 并缺少迭代

标签 java multithreading javafx

我正在用Java编写一个程序,它使用遗传算法将一组数字分成两组,以便它们的总和相等或尽可能接近相等。该程序应该将每次迭代的结果附加到 TextArea 中。我不希望程序在进行计算时卡住,因此我将逻辑放在另一个线程中。更具体地说:

  1. 我创建了扩展 Task 的 Runner 类。 BestSpecimen 是我的类,它在这里用于打印迭代编号和本次迭代中找到的最佳解决方案列表。它还更新了 ProgressBas,但这并不那么重要。我通过在 Controller 类中添加以下行来做到这一点: runnerProgressPB.progressProperty().bind(runner.progressProperty());
  2. 我想对 TextArea 进行同样的操作,但它无法正常工作。关键是它用当前迭代更新了 TextArea,但删除了先前迭代的结果。
  3. 我使用了不同的方法:在我的 Controller 类中我添加了这段代码: runner.valueProperty().addListener((observable, oldValue, newValue) -> { resultsTA.appendText(newValue + "\n"); });
  4. 很简单,它只是在名为 runner 的线程中监听 ValueProperty 的变化。我在创建 BestSpecimen 对象后更新了 ValueProperty,该对象发生在迭代中间。然而,问题来了 - TextArea 中缺少许多迭代。看一下屏幕截图。某一时刻有超过 100 次迭代缺失! Screenshot
  5. 嗯,我认为发生了什么?线程运行速度太快,以至于 TextArea 无法及时更新信息。我在 for 循环中放入了一些 Thread.sleep() ,这减少了丢失迭代的数量,但程序运行速度非常慢!我使用了 Thread.sleep(20),花了 5-10 分钟才能完成,并且仍然缺少一些迭代。这不是解决方案。
  6. 我想实现什么目标?我希望运行程序线程在设置 ValueProperty 后挂起并等待 TextArea 不更新。我想我应该使用 wait()/notification() 构造,但不知 Prop 体在哪里。我对线程方面还很陌生......
  7. 这是我的 Runner 类的 call() 方法中的主循环。在它之前,只有变量的声明。

        while (!this.done) {
            for (int i = 0; i < this.iterations; i++) {
                current = this.population.get(i);
                theBestOnes = current.getTheBestSpecimen();
                bestCase = new BestSpecimen(i+1, theBestOnes);
                this.updateValue(bestCase);
                this.updateProgress(i, iterations);
                next = Population.createParentPopulationFromExistingOne(current);
                next.fillPopulation();
                this.population.add(this.population.size(), next);
            }
            done = true;
        }
        return null;
    
  8. 顺便说一句,还有一个令人讨厌的空值(参见上面的屏幕截图),它被附加到 TextArea 中。我有什么想法可以摆脱它吗?方法末尾需要 return 语句,我无法返回没有内容的 BestSpecimen 对象。

最佳答案

updateXXX Task中的方法完全按照他们所说的去做:他们更新属性的值。更新在 FX 应用程序线程上执行。由于目的是提供一个可以在 UI 中观察到的值,因此如果该值在帧渲染之间多次更改,则实际上只会使用帧每次渲染时的最新值。 documentation 中明确说明了这一点:

Updates the value property. Calls to updateValue are coalesced and run later on the FX application thread, so calls to updateValue, even from the FX Application thread, may not necessarily result in immediate updates to this property, and intermediate values may be coalesced to save on event notifications.

最简单的解决方案如下(尽管由于下述原因它可能表现不佳:

while (!this.done) {
    for (int i = 0; i < this.iterations; i++) {
        current = this.population.get(i);
        theBestOnes = current.getTheBestSpecimen();
        bestCase = new BestSpecimen(i+1, theBestOnes);

        final String text = bestCase.toString()"+\n";
        Platform.runLater(() -> resultsTA.appendText(text));

        this.updateProgress(i, iterations);
        next = Population.createParentPopulationFromExistingOne(current);
        next.fillPopulation();
        this.population.add(this.population.size(), next);
    }
    done = true;
}
return null;

并且显然摆脱了 valueProperty 的监听器.

这里的问题是,您最终可能会为 FX 应用程序线程安排许多操作,并且最终可能会导致大量工作淹没它,以至于它无法执行重新绘制 UI 和处理用户事件等常规任务。

解决这个问题有点棘手。一种选择是使用 BlockingQueue<String>将更新排队,以及 AnimationTimer使用它们更新 UI:

BlockingQueue<String> textQueue = new LinkedBlockingQueue<>();

// ...

int iterations = ... ;

现在在你的任务中执行

for (int i = 0; i < this.iterations; i++) {
    current = this.population.get(i);
    theBestOnes = current.getTheBestSpecimen();
    bestCase = new BestSpecimen(i+1, theBestOnes);

    final String text = bestCase.toString()+"\n";
    textQueue.put(text);

    this.updateProgress(i, iterations);
    next = Population.createParentPopulationFromExistingOne(current);
    next.fillPopulation();
    this.population.add(this.population.size(), next);
}
return null;

当您启动任务时,也会启动 AnimationTimer如下:

AnimationTimer timer = new AnimationTimer() {

    private int updates = 0 ;
    @Override
    public void handle(long now) {
        List<String> newStrings = new ArrayList<>();
        updates += textQueue.drainTo(newStrings);
        StringBuilder sb = new StringBuilder();
        newStrings.forEach(sb::append);
        resultsTA.appendText(sb.toString());
        if (updates >= iterations) {
            stop();
        }
    }
};

timer.play();

即使这样也可能表现不佳,因为可能有大量文本(以及大量字符串连接来构建它)。您可以考虑使用ListView而不是TextArea ,在这种情况下,您可以直接将队列排空到 ListView 的项目。

关于JavaFX - 从另一个线程更新 TextArea 并缺少迭代,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33681623/

相关文章:

java - 不匹配的 Javafx fx :id and variable name in controller class

java - 我可以从 javaFX 中的 setOnAction 参数调用另一个类吗?

Java 多播套接字未在特定网络接口(interface)上接收

c++ - 不同线程数 openMP 上的相同程序执行时间

android - RxJava2如何观察UDP数据包?

Java并发线程错误

java - 创建无限短生命周期线程

java - Quartz 预定作业未触发 - 可能未处理的异常?

java - 如何从字符串生成 16 x 16 矩阵?

java - Hibernate Criteria API 结果缩小外键范围