What is the best way to manage multithreading in JavaFX 8?

16,294

Solution 1

In addition to using the low-level Thread API and Platform.runLater(...) to schedule code to be executed on the FX Application Thread, as in Tomas' answer, another option is to use the FX concurrency API. This provides Service and Task classes, which are intended to be executed on background threads, along with callback methods which are guaranteed to be executed on the FX Application Thread.

For your simple example, this looks like a bit too much boilerplate code, but for real application code it is quite nice, providing a clean separation between the background task and the UI update that is performed on completion. Additionally, Tasks can be submitted to Executors.

import javafx.application.Application;
import javafx.concurrent.Task ;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class ClassApplication extends Application {

    private Pane pane;

    public Parent createContent() {

        /* layout */
        BorderPane layout = new BorderPane();

        /* layout -> center */
        pane = new Pane();
        pane.setMinWidth(250);
        pane.setMaxWidth(250);
        pane.setMinHeight(250);
        pane.setMaxHeight(250);
        pane.setStyle("-fx-background-color: #000000;");

        /* layout -> center -> pane */
        Circle circle = new Circle(125, 125, 10, Color.WHITE);

        /* add items to the layout */
        pane.getChildren().add(circle);

        layout.setCenter(pane);
        return layout;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setWidth(300);
        stage.setHeight(300);
        stage.show();

        Task<Void> task = new Task<Void>() {
            @Override
            public Void call() throws Exception {
                Thread.sleep(2000);
                return null ;
            }
        };

        task.setOnSucceeded(event -> {
            Circle circle = new Circle(50, 50, 10, Color.RED);
            pane.getChildren().setAll(circle);
        });

        new Thread(task).run();
    }

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

If all you are doing is pausing, you can even get away with using (or abusing?) the animation API. There's a PauseTransition that pauses for a specified time, and you can use its onFinished handler to execute the update:

import javafx.application.Application;
import javafx.animation.PauseTransition ;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration ;

public class ClassApplication extends Application {

    private Pane pane;

    public Parent createContent() {

        /* layout */
        BorderPane layout = new BorderPane();

        /* layout -> center */
        pane = new Pane();
        pane.setMinWidth(250);
        pane.setMaxWidth(250);
        pane.setMinHeight(250);
        pane.setMaxHeight(250);
        pane.setStyle("-fx-background-color: #000000;");

        /* layout -> center -> pane */
        Circle circle = new Circle(125, 125, 10, Color.WHITE);

        /* add items to the layout */
        pane.getChildren().add(circle);

        layout.setCenter(pane);
        return layout;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setWidth(300);
        stage.setHeight(300);
        stage.show();

        PauseTransition pause = new PauseTransition(Duration.millis(2000));
        pause.setOnFinished(event -> pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED)));
        pause.play();
    }

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

If you need to execute the pause multiple times, you can use a Timeline, and call setCycleCount(...):

import javafx.application.Application;
import javafx.animation.Timeline ;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration ;

public class ClassApplication extends Application {

    private Pane pane;

    public Parent createContent() {

        /* layout */
        BorderPane layout = new BorderPane();

        /* layout -> center */
        pane = new Pane();
        pane.setMinWidth(250);
        pane.setMaxWidth(250);
        pane.setMinHeight(250);
        pane.setMaxHeight(250);
        pane.setStyle("-fx-background-color: #000000;");

        /* layout -> center -> pane */
        Circle circle = new Circle(125, 125, 10, Color.WHITE);

        /* add items to the layout */
        pane.getChildren().add(circle);

        layout.setCenter(pane);
        return layout;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setWidth(300);
        stage.setHeight(300);
        stage.show();

        KeyFrame keyFrame = new KeyFrame(Duration.millis(2000), 
            event -> pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED)));
        Timeline timeline = new Timeline(keyFrame);

        // Repeat 10 times:
        timeline.setCycleCount(10);

        timeline.play();
    }

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

Solution 2

You can access the scene only from the JavaFX application thread. You need to wrap your code that accesses the scene graph in Platform.runLater():

Platform.runLater(() -> {
    Circle circle = new Circle(50, 50, 10, Color.RED);
    pane.getChildren().clear();
    pane.getChildren().add(circle);
});
Share:
16,294
bluevoxel
Author by

bluevoxel

Updated on July 24, 2022

Comments

  • bluevoxel
    bluevoxel almost 2 years

    I'm trying to find an efficient way to influence the shape and the content of the JavaFX GUI elements, such as simple Pane, with use of multithreading. Let's say I have a simple Pane, on which I display filled Circles at the given itervals of time, and I want to have the posibility to answer to them, e.g. by hitting the corresponding key. So far, for this purpose, I tried to use class with the implementation of Runnable interface and creation of classic Thread object in it, in order to add and/or remove elements from the external JavaFX Pane, which was passed to that "thread class" in its constructor parameter from the main Application class. Say, both classes, 1) application and 2) thread class, looks like this:

    import javafx.application.Application;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;
    
    public class ClassApplication extends Application {
    
        private Pane pane;
    
        public Parent createContent() {
    
            /* layout */
            BorderPane layout = new BorderPane();
    
            /* layout -> center */
            pane = new Pane();
            pane.setMinWidth(250);
            pane.setMaxWidth(250);
            pane.setMinHeight(250);
            pane.setMaxHeight(250);
            pane.setStyle("-fx-background-color: #000000;");
    
            /* layout -> center -> pane */
            Circle circle = new Circle(125, 125, 10, Color.WHITE);
    
            /* add items to the layout */
            pane.getChildren().add(circle);
    
            layout.setCenter(pane);
            return layout;
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(createContent()));
            stage.setWidth(300);
            stage.setHeight(300);
            stage.show();
    
            /* initialize custom Thread */
            ClassThread thread = new ClassThread(pane);
            thread.execute();
        }
    
        public static void main(String args[]) {
            launch(args);
        }
    }
    

    ...and the "thread class"

    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    
    public class ClassThread implements Runnable {
    
        private Thread t;
        private Pane pane;
    
        ClassThread(Pane p) {
            this.pane = p;
    
            t = new Thread(this, "Painter");
        }
    
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                Circle circle = new Circle(50, 50, 10, Color.RED);
                pane.getChildren().clear();
                pane.getChildren().add(circle);
    
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            }
        }
    
        public void execute() {
            t.start();
        }
    }
    

    However, such a solution, where in Swing application could be possible, in JavaFX is impossible, and what's more, is the reason of the following exception:

    Exception in thread "Painter" java.lang.IllegalStateException: Not on FX application thread; currentThread = Painter
        at com.sun.javafx.tk.Toolkit.checkFxUserThread(Unknown Source)
        at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(Unknown Source)
        at javafx.scene.Parent$2.onProposedChange(Unknown Source)
        at com.sun.javafx.collections.VetoableListDecorator.clear(Unknown Source)
        at ClassThread.run(ClassThread.java:21)
        at java.lang.Thread.run(Unknown Source)
    

    ...where the line "21" is: pane.getChildren().clear();

    I've concluded, that "there is a problem with influencing the main JavaFX thread from the level of another thread". But in this case, how can I change the JavaFX GUI elements shape and content dynamically, if I can't (tbh more accurately to say will be "I don't know how") bind togheter few threads?

    UPDATE : 2014/08/07, 03:42

    After reading given answers I tried to implement given solutions in code, in order to display 10 custom Circles on different locations with specified time intervals between each display:

    /* in ClassApplication body */
    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setWidth(300);
        stage.setHeight(300);
        stage.show();
    
        Timeline timeline = new Timeline();
        for (int i = 0; i < 10; i++) {
            Random r = new Random();
            int random = r.nextInt(200) + 25;
            KeyFrame f = new KeyFrame(Duration.millis((i + 1) * 1000), 
                new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent ae) {
                    pane.getChildren().add(new Circle(
                        random, random, 10, Color.RED));
                }
            });
            timeline.getKeyFrames().add(f);
        }
        timeline.setCycleCount(1);
        timeline.play();
    }
    

    The solution above works just fine. Thank you very much.

  • Tomas Mikula
    Tomas Mikula over 9 years
    And if the abuse of the animation API is still too much boilerplate, ReactFX has a wrapper around it: FxTimer.runLater(Duration.ofMillis(2000), () -> pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED)));
  • bluevoxel
    bluevoxel over 9 years
    @James_D The problem with this solution is that, it is good for a single delayed events. But in case of displaying multiple Circles in specific intervals of time it's problematic. Say, I have 10 Circles and I want them to be displayed one-by-one in, e.g. 2000ms intervals on the same Pane. I also tried to use Timeline and it's KeyValue and KeyFrame, but the problem is, that in KeyValue object I can't add and remove elements from the Pane.
  • James_D
    James_D over 9 years
    Added an example which repeats 10 times. You can set the cycle count to Animation.INDEFINITE to repeat indefinitely. And @TomasMikula no doubt has a one-liner from ReactFX for the same thing ;).
  • Tomas Mikula
    Tomas Mikula over 9 years
    @James_D To repeat indefinitely, just change runLater in my previous comment to runPeriodically. I don't have an easier solution for a fixed number of repetitions. I imagine something like this: EventStreams.ticks(Duration.ofMillis(2000)).limit(10).subscr‌​ibe(x -> pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED))), except the limit operator is not there. It's just no one has asked for it yet :)
  • bluevoxel
    bluevoxel over 9 years
    @James_D, @Tomas Mikula, I've tried to implement your solutions in some piece of code in my question update, but I have another small problem with the multiple custom KeyFrame initialization, adding, and execution in the Timeline object. The problem is, that specified EventHandlers works fine, but the Duration value is respected only for one of the added KeyFrame.
  • Tomas Mikula
    Tomas Mikula over 9 years
    @bluevoxel In your code, you are adding 10 KeyFrames, all of which execute at time=1000ms. If you want to use 10 key frames instead of 10 cycles, then use Duration.millis((i+1) * 1000). KeyFrame's duration is paradoxically not duration of the KeyFrame, but duration since the start of the animation.