【问题标题】:What things need to keep in Mind while developing multithread Javafx application?开发多线程 Javafx 应用程序时需要牢记什么?
【发布时间】:2014-10-04 10:11:07
【问题描述】:

我是 Javafx 的新手,并使用它开发了一个 IDE。 JavaFX 面临的问题是我必须使用Platform.runLater() 来反映来自其他线程的GUI 变化。作为我正在开发的 IDE,我使用多个线程来保持最新信息,并且使用 Platform.runLater() 会使应用程序无响应。有时后台进程必须打印数百万行的输出,我认为当多个线程尝试执行相同操作时会导致问题。我尝试放置一个计数器,以便如果输出大于 250000 行,它将在 250000 行之后打印输出,否则在线程完成后立即打印,即使在这种情况下,如果两个或多个线程尝试执行 @987654324 @(还有其他线程创建带有复选框项目并反映实时值的树)应用程序挂起,但后台的所有内容都保持正常运行,甚至应用程序也不会抛出任何异常。在普通的 java swing 应用程序中,我没有遇到任何类似的问题。所以我正在寻求指导来解决这些问题。有人可以给我解决类似问题的专业提示吗? :)

应@jewelsea 的要求编辑

我试图让示例代码尽可能简单

FxUI.java

 public class FxUI extends Application {
 public static TextArea outputArea;

@Override
public void start(Stage primaryStage) {
    outputArea= new TextArea();
    Button btn = new Button();

    btn.setText("Start Appending Text To Text Area");
    btn.setOnAction(new EventHandler<ActionEvent>() {

        @Override
        public void handle(ActionEvent event) {
          Thread r=new Thread( new Runnable() {

               @Override
               public void run() {
              for (int i = 0; i < 10; i++) {
                Thread t= new Thread(new simpleThread(i));
            t.start();
                try {
                    Thread.sleep(1000);
                    System.out.println("Thread Awake");
                } catch (InterruptedException ex) {
                    Logger.getLogger(FxUI.class.getName()).log(Level.SEVERE, null, ex);
                }
            }  }
           });
          r.start();
        }
    });

    VBox root = new VBox(30);
    outputArea.setWrapText(true);
    outputArea.setPrefHeight(400);
    root.getChildren().add(outputArea);
    root.getChildren().add(btn);

    Scene scene = new Scene(root, 500, 500);

    primaryStage.setTitle("Hello World!");
    primaryStage.setScene(scene);
    primaryStage.show();
}

/**
 * @param args the command line arguments
 */
public static void main(String[] args) {
    launch(args);
}

}

simpleThread.java

 public class simpleThread implements Runnable {

int threadnumber;

public simpleThread(int j) {
    threadnumber = j;
}

@Override
public void run() {
    String output = "";
    String content;
    int length;
    final String finalcontent2;
    final int finallength2;

    for (long i = 0L; i <= 10000; i++) {
        final String finalcontent;
        final int finallength;

        if (i % 1000 == 0) {
            output += "\nThread number = " + threadnumber + " \t Loop Counter=" + i;
            content = FxUI.outputArea.getText() + "\n" + output;
            length = content.length();
            finallength = length;
            finalcontent = "" + content;
            Platform.runLater(new Runnable() {

                @Override
                public void run() {
                    System.out.println("appending output");
                    FxUI.outputArea.setText(finalcontent);
                    FxUI.outputArea.positionCaret(finallength);

                }
            });
        } else {
            output += "\nThread number = " + threadnumber + " \t Loop Counter=" + i;

        }
        System.out.println("Thread number = " + threadnumber + " \t Loop Counter=" + i);

    }

}

}

【问题讨论】:

  • 您应该提供mcve
  • @jewelsea 新编辑,试图复制问题:)
  • @jewelsea 有什么建议吗? :)

标签: java multithreading java-8 javafx-8


【解决方案1】:

我认为这里的第一个真正问题是您的代码效率极低。在循环中构建字符串是一件非常糟糕的事情:您创建一个新对象并每次都复制所有字符。此外,每次更新文本区域时,都会复制整个现有文本,通过连接其他内容创建另一个 String,然后用新内容替换所有现有内容。字符串连接将以二次时间运行(因为您每次都在增加字符串的长度)并且您将给 Java 的字符串实习过程造成混乱。

另外,请注意,除了 FX 应用程序线程之外,您不应在任何地方读取场景图中节点的状态,因此您的行

        content = FxUI.outputArea.getText() + "\n" + output;

不是线程安全的。

一般来说,要在循环中构建字符串,您应该使用StringBuilder 来构建字符串内容。如果您使用的是TextArea,它有一个appendText(...) 方法,您只需更新它即可。

更新以下 cmets 中的讨论:

做出了那些通用的 cmets,做出这些改进并不能真正让您达到性能可以接受的状态。我的观察是TextArea 对用户输入的响应速度很慢,即使在线程完成后也是如此。问题是(我猜)您有大量数据实际上与场景图的“实时”部分相关联。

这里更好的选择可能是使用虚拟化控件(例如ListView)来显示数据。这些只有可见部分的单元格,并在用户滚动时重复使用它们。这是一个例子。我添加了选择和复制到剪贴板功能,因为这是从TextAreaListView 的主要功能。 (请注意,如果您选择了大量的东西,String.join() 方法运行起来会很慢。如果这很重要,您可能需要为此创建一个后台任务和一个阻止对话框来显示其进度。)

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class BigListBackgroundThreadDemo extends Application {

    private static final int NUM_ITERATIONS = 10_000 ;
    private static final int NUM_THREADS_PER_CALL = 5 ;

    @Override
    public void start(Stage primaryStage) {
        ListView<String> data = new ListView<>();
        data.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        Button startButton = new Button("Start");
        Button selectAllButton = new Button("Select All");
        Button selectNoneButton = new Button("Clear Selection");
        Button copyToClipboardButton = new Button("Copy to clipboard");
        copyToClipboardButton.disableProperty().bind(Bindings.isEmpty(data.getSelectionModel().getSelectedItems()));

        AtomicInteger threadCount = new AtomicInteger();
        ExecutorService exec = Executors.newFixedThreadPool(5, r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t ;
        });

        startButton.setOnAction(event -> {
            exec.submit(() -> {
                for (int i=0; i < NUM_THREADS_PER_CALL; i++) {
                    exec.submit(createTask(threadCount, data));
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException exc) {
                        throw new Error("Unexpected interruption", exc);
                    }
                }
            });
        });

        selectAllButton.setOnAction(event -> {
            data.getSelectionModel().selectAll();
            data.requestFocus();
        });
        selectNoneButton.setOnAction(event -> {
            data.getSelectionModel().clearSelection();
            data.requestFocus();
        });

        copyToClipboardButton.setOnAction(event -> {
            ClipboardContent clipboardContent = new ClipboardContent();
            clipboardContent.putString(String.join("\n", data.getSelectionModel().getSelectedItems()));
            Clipboard.getSystemClipboard().setContent(clipboardContent);
        });

        HBox controls = new HBox(5, startButton, selectAllButton, selectNoneButton, copyToClipboardButton);
        controls.setAlignment(Pos.CENTER);
        controls.setPadding(new Insets(5));

        BorderPane root = new BorderPane(data, null, null, controls, null);


        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Task<Void> createTask(AtomicInteger threadCount, ListView<String> target) {
        return new Task<Void>() {
            @Override
            public Void call() throws Exception {
                int count = threadCount.incrementAndGet();
                AtomicBoolean pending = new AtomicBoolean(false);
                BlockingQueue<String> messages = new LinkedBlockingQueue<>();
                for (int i=0; i < NUM_ITERATIONS; i++) {
                    messages.add("Thread number: "+count + "\tLoop counter: "+i);
                    if (pending.compareAndSet(false, true)) {
                        Platform.runLater(() -> {
                            pending.set(false);
                            messages.drainTo(target.getItems());
                            target.scrollTo(target.getItems().size()-1);
                        });
                    }
                }

                return null ;
            }
        };
    }


    public static void main(String[] args) {
        launch(args);
    }
}

【讨论】:

  • “将 Runnable 发布到事件队列,然后立即返回给调用者” 我认为使用 javafx.concurrent 无济于事,因为它将返回给调用者和所有线程将立即执行,但它将根据其在队列中的位置发布输出。感谢您的回复,我们也可以使用FxUI.outputArea.getLength() 而不是FxUI.outputArea.getText().length()
  • 我认为 javafx 更适合小型应用程序,但我们不能考虑将它用于开发我正在尝试做的大规模或超大规模应用程序。我对吗?如果您与我分享您的想法,我会很高兴。我不是专家,但我从未在 LS 或 VLS swing 应用程序中遇到过这样的问题。如果 javafx 不能实时向我们显示值,它会有什么用处?
  • 我不太明白你的第一条评论。至于大规模应用,我想这可能取决于你所说的大规模。我使用 JavaFX 来显示相当大的数据集(基因组学;所以我们一次处理引用数百万条记录的表)并且性能很好。我对您的代码的观察是,即使在线程完成后,它实际上也表现得很糟糕。因此,当您拥有大量数据时,TextArea 可能不是 UI 的好选择。考虑改用ListView 来实现。
  • :) 你偷走了我脑海中的想法(因为所有的功劳都会归你所有),我打算这样做,但我试图与TextArea 合作。在我两年前设计的一个摇摆应用程序中,我使用JList 来表示冗长的数据。大规模应用程序意味着我正在开发的应用程序是一个并行计算框架,当考虑在这里和那里运行的多个线程以及多台机器相互通信时,我认为它非常大。我面临的问题是以图形方式实时表示数据。 Javafx 需要花费大量时间来反映 GUI 上的变化。
  • 我选择TextArea bcz 是因为它可以灵活地选择输出。
【解决方案2】:

在 JavaFX 中,您必须在运行 TaskService 中执行后台进程。通过这样做,您将不会冻结您的 GUI 线程

简单的例子,如果你想要一个字符串作为你的进程的返回值。

服务:

    public class MyService extends Service<String> {
    @Override
    protected Task<String> createTask() {
        return new Task<String>() {
            @Override
            protected String call() throws Exception {
                //Do your heavy stuff
                return "";
            }
        };
    }
}

您想使用服务的地方:

    final MyService service = new MyService();

    service.setOnSucceeded(e -> {
        //your service finish with no problems
        service.getValue(); //get the return value of your service
    });

    service.setOnFailed(e -> {
        //your service failed
    });

    service.restart();

对于不同的状态,您还有其他方法,例如 setOnFailed。所以实现你所需要的。 您也可以监视此服务,但我让您为此阅读文档。很简单。

您还应该阅读JavaFX concurency

【讨论】:

  • 我认为,您应该使用 lambdas 复制这些示例,它更短且更易于阅读。
  • 顺便说一句,如果您的任务没有返回某些内容,通常使用Void 进行参数化。
  • 在服务中包装一个任务实际上并不是必需的,事实上这会使在应用程序关闭期间停止所有剩余任务变得更加困难
  • @agonist_ 实现服务代替普通 java 线程会有什么不同? bcz 我所有的后台进程都在 Java 线程上运行,除了更新输出和树视图的文本区域外,我的 UI 线程上没有任何繁重的运行。在写这篇评论时,我想到了一件事,我必须从 textarea 获取所有文本然后每次都将我的输出附加到它,如果我要打印数百万行输出,可能是 javafx 变得无响应,因为我在 Platorm.runlater 中获取了数百万个文本并将文本附加到相同的函数中......我想是的....
  • @agonist_ Platform Documentation 根据这一点,您可以从任何线程调用此方法,并且必须尽可能缩短调用它的时间。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-21
相关资源
最近更新 更多