JavaFX - 从另一个线程更新TextArea并丢失迭代

时间:2015-11-12 21:22:18

标签: java multithreading javafx

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

  1. 我创建了一个扩展Task的类Runner。 BestSpecimen是我的类,它用于打印迭代次数和此迭代中找到的最佳解决方案列表。它也更新ProgressBas,但它并不重要。我通过在我的控制器类中添加以下行来完成它:runnerProgressPB.progressProperty()。bind(runner.progressProperty());
  2. 我想在更新TextArea时也这样做,但它无法正常工作。关键是它使用当前迭代更新了TextArea,但删除了之前迭代的结果。
  3. 我使用了不同的方法:在我的控制器类中,我添加了这段代码: 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()/ notify()构造,但dunno究竟在哪里。我在线程方面很新......
  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. 顺便说一句,还有这个讨厌的null(参见上面的截图),它附加在TextArea上。我有什么想法可以摆脱它吗?返回语句在方法结束时重新生成,我无法返回没有内容的BestSpecimen对象。

1 个答案:

答案 0 :(得分:0)

updateXXX中的Task方法正是他们所说的:他们更新了属性的值。更新在FX应用程序线程上执行。由于意图是提供可以在UI中观察到的值,如果在帧渲染之间多次改变值,则实际上仅使用帧的每次渲染时的最新值。这在documentation

中明确说明
  

更新value属性。对updateValue的调用被合并并运行   稍后在FX应用程序线程上,所以调用updateValue,甚至来自   FX应用程序线程可能不一定立即导致   对此属性的更新,可以合并中间值   保存事件通知。

最简单的解决方案如下(尽管由于以下原因可能效果不佳:

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更新用户界面:

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,在这种情况下,您可以直接将队列排到列表视图的项目中。