1

I'm learning multithreads with progressbar. I have got update without problems in the following simple code:

public class Controller implements Initializable {
@FXML
private Button id_boton1;
@FXML
private Button id_boton2;

@FXML
private ProgressBar id_progressbar1;
@FXML
private ProgressIndicator id_progressindicator1;

@Override
public void initialize(URL location, ResourceBundle resources) {
    id_progressbar1.setProgress(0.0);
    id_boton1.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            Thread hiloProgressBar= new Thread(new bg_Thread1());
            //hilo.setDaemon(true);
            hiloProgressBar.start();
        }
    });

    id_boton2.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            Thread hiloProgressIndicate = new Thread(new bg_Thread2());
            hiloProgressIndicate.start();
        }
    });



}

class bg_Thread1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            id_progressbar1.setProgress((float)i/99);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);

        }
    }
}

class bg_Thread2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            id_progressindicator1.setProgress((float)i/99);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);

        }
    }
}}

Nothing special. 2 buttons that create thread with a job and progressBar.

My question is about the implementation of CountDownLatch to this program. I want to wait for the two threads to finish. Now the UI does not refresh with the progressbar changes

public class Controller implements Initializable {
@FXML
private Button id_boton1;


@FXML
private ProgressBar id_progressbar1;
@FXML
private ProgressIndicator id_progressindicator1;

@Override
public void initialize(URL location, ResourceBundle resources) {



    CountDownLatch countDownLatch = new CountDownLatch(2);

    id_progressbar1.setProgress(0.0);
    id_boton1.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {

            System.out.println("iniciando Threads");

            Thread hiloProgressBar= new Thread(new bg_Thread1(countDownLatch));
            hiloProgressBar.start();

            Thread hiloProgressIndicate = new Thread(new bg_Thread2(countDownLatch));
            hiloProgressIndicate.start();

            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("*fin*");
        }
    });


}

class bg_Thread1 extends Thread{

    private CountDownLatch latch;

    public bg_Thread1(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            id_progressbar1.setProgress((float)i/99);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);

        }
        latch.countDown();
    }
}

class bg_Thread2 extends Thread{

    private CountDownLatch latch;

    public bg_Thread2(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        for (int i = 0; i < 40; i++) {
            id_progressindicator1.setProgress((float)i/39);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);

        }
        latch.countDown();
    }
}}

Now, One button start two threads, when the work is finish the progressBar is updated one time

NotZack
  • 518
  • 2
  • 9
  • 22
agriarte
  • 75
  • 8
  • 1
    Check out [Concurrency in JavaFX](https://docs.oracle.com/javase/8/javafx/interoperability-tutorial/concurrency.htm). Your problem is you're blocking the _JavaFX Application Thread_, which you should never do. – Slaw Aug 23 '19 at 12:26
  • Thank you Slaw. Yes, the problem is about JavaFX Application Thread. Unfortunately, I can't find the solution. – agriarte Aug 23 '19 at 13:50
  • Do you have to use the CountdownLatch? – SedJ601 Aug 23 '19 at 13:54
  • Unrelated to your problem: please learn java naming conventions and stick to them. you are nearly there, now replace the underscores by camel-case and it's perfect :) – kleopatra Aug 23 '19 at 14:08
  • I would like use CountdownLach or another equivalent. – agriarte Aug 23 '19 at 14:11

2 Answers2

2

A CountDownLatch is good for example when you want to divide a SQL query up into smaller queries, and wait for all the queries to come back. Then combine your results only after all of them have finished. I'm not sure if you really need it or not... you can just add an event handler when the state has "succeeded". For this example, I changed a label to "Finished" once the respective thread had completed. If you need to wait for all of them to finish to do something else, then you would need to encapsulate the latch in yet another thread. latch.await() if run on the UI thread will freeze the UI.

Update: I've implemented a CountDownLatch since you need to wait for both threads to complete to do something else.


enter image description here enter image description here enter image description here enter image description here


import java.util.concurrent.CountDownLatch;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.VBox;
import javafx.stage.*;

public class ProgressTest extends Application
{

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

    @Override
    public void start(Stage primaryStage)
    {

        Button getButton = new Button("Copy");

        ProgressBar progressBar = new ProgressBar(0);
        ProgressIndicator progressIndicator = new ProgressIndicator(0);
        progressBar.setProgress(0);
        progressIndicator.setProgress(0);
        Label statusLabel1 = new Label();
        Label statusLabel2 = new Label();
        VBox vBox = new VBox();
        vBox.getChildren().addAll(statusLabel1, statusLabel2, progressBar, progressIndicator, getButton);
        vBox.setAlignment(Pos.CENTER);

        getButton.setOnAction((ActionEvent e) ->
        {

            CountDownLatch countDownLatch = new CountDownLatch(2);

            // Create a Task.
            CopyTask copyTask1 = new CopyTask(30, countDownLatch);
            CopyTask copyTask2 = new CopyTask(50, countDownLatch);
            progressBar.progressProperty().unbind();
            // Bind progress property
            progressBar.progressProperty().bind(copyTask2.progressProperty());

            progressIndicator.progressProperty().unbind();
            // Bind progress property.
            progressIndicator.progressProperty().bind(copyTask1.progressProperty());

            // Unbind text property for Label.
            statusLabel1.textProperty().unbind();
            statusLabel2.textProperty().unbind();

            // Bind the text property of Label
            // with message property of Task
            statusLabel1.textProperty().bind(copyTask1.messageProperty());
            statusLabel2.textProperty().bind(copyTask2.messageProperty());

            // When completed tasks
            copyTask1.addEventHandler(WorkerStateEvent.WORKER_STATE_SUCCEEDED, (WorkerStateEvent t) ->
            {
                statusLabel1.textProperty().unbind();
                statusLabel1.setText("Finished1");
            });
            copyTask2.addEventHandler(WorkerStateEvent.WORKER_STATE_SUCCEEDED, (WorkerStateEvent t) ->
            {
                statusLabel2.textProperty().unbind();
                statusLabel2.setText("Finished2");

            });

            Task<Void> task = new Task<Void>()
            {
                @Override
                public Void call() throws InterruptedException
                {

                    // Start the Task.
                    new Thread(copyTask1).start();
                    new Thread(copyTask2).start();
                    countDownLatch.await();
                    return null;

                }
            };

            task.setOnSucceeded(event ->
            {
                System.out.println("This occurs after both threads complete...");

            });

            task.setOnFailed(event ->
            {
                System.out.println("FAIL...");
            });

            Thread thread = new Thread(task);
            thread.start();

        });

        final Scene scene = new Scene(vBox);

        primaryStage.setScene(scene);

        primaryStage.show();
    }

    public class CopyTask extends Task<Void>
    {

        int x;
        CountDownLatch latch;

        public CopyTask(int x, CountDownLatch latch)
        {
            super();
            this.x = x;
            this.latch = latch;

        }

        @Override
        protected Void call() throws Exception
        {
            //don't do the infitnite progress meter ...
            this.updateProgress(0, x);

            try
            {

                int len;
                for (len = 0; len <= x; len++)
                {
                    this.updateProgress(len, x);
                    Thread.sleep(100);
                    if (this.isCancelled())
                    {
                        throw new InterruptedException();
                    }
                }

                if (this.isCancelled())
                {
                    throw new InterruptedException();
                }
            } catch (InterruptedException ex)
            {
                System.out.println("Cancelled");
            }
            latch.countDown();
            return null;

        }

    }

}
trilogy
  • 1,738
  • 15
  • 31
  • Your code is very interesting. Really, I need to wait for all of them to finish to do something, I have work to try modify this code. You say that is necessary to encapsulate the latch in yet another thread. Now, I understand that CountdownLatch is not compatible with the UI thread, its true? Thanks a lot. I will study your example. Because my low reputation I can't vote your asnswer. – agriarte Aug 23 '19 at 19:52
  • @agriarte Correct, objects such as `CountDownLatch` are not compatible with use on the FX thread (though a background thread could wait for the FX thread to call `countDown()`, i.e. the reverse of what you're trying to do). I've added an answer showing how you can wait for multiple tasks without using a `CountDownLatch`. – Slaw Aug 24 '19 at 01:41
2

When you block on the CountDownLatch waiting for each task to finish you are blocking the JavaFX Application Thread. That thread is responsible for processing the UI and user events, which means if it's blocked none of that can happen. This is why your UI is frozen while waiting for the tasks to finish. For more information, see Concurrency in JavaFX.

The page I linked to showcases the javafx.concurrent API. It contains classes that are capable of communicating with the FX thread in a thread-safe manner. The Javadoc of the classes and interfaces in that API explain how to use them in detail. In your case, you're interested in the Worker#progress property, which you can bind the ProgressBar#progress property to. You'd then use Task#updateProgress to post updates to the FX thread which will cause the ProgressBar to update.

However, you also need a way to do something once every task has finished. You're currently doing this with a CountDownLatch to wait for each task. This is problematic, as already discussed. When programming a GUI application you need to think in a more event-driven fashion. In other words, instead of waiting for the tasks to finish you should react to when they finish. Keep track of how many tasks there are and, when all are finished, execute some action. Here's an example:

import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {

    private static final int TASK_COUNT = 6;

    private static List<Task<?>> createTasks() {
        Random random = new Random();
        return IntStream.range(0, TASK_COUNT)
                .mapToObj(i -> new MockTask(random.nextInt(4_000) + 1_000))
                .collect(Collectors.toList());
    }

    private static boolean isTerminalState(Worker.State state) {
        return state == Worker.State.SUCCEEDED
                || state == Worker.State.CANCELLED
                || state == Worker.State.FAILED;
    }

    // Assumes none of the tasks are completed yet. May wish to validate that.
    private static void onTasksComplete(Runnable action, List<? extends Task<?>> tasks) {
        // local variables must be effectively final when used in lambda expressions
        int[] count = new int[]{tasks.size()}; 
        for (Task<?> task : tasks) {
            task.stateProperty().addListener((observable, oldState, newState) -> {
                if (isTerminalState(newState) && --count[0] == 0) {
                    action.run(); // invoked on FX thread
                }
            });
        }
    }

    @Override
    public void start(Stage primaryStage) {
        List<Task<?>> tasks = createTasks();

        VBox root = new VBox(10);
        root.setPadding(new Insets(15));
        root.setAlignment(Pos.CENTER);

        for (Task<?> task : tasks) {
            ProgressBar progressBar = new ProgressBar();
            progressBar.setMaxWidth(Double.MAX_VALUE);
            // In a real application you'd probably need to unbind when the task completes
            progressBar.progressProperty().bind(task.progressProperty());
            root.getChildren().add(progressBar);
        }

        primaryStage.setScene(new Scene(root, 500, -1));
        primaryStage.setTitle("Concurrency");
        primaryStage.show();

        onTasksComplete(() -> {
            Alert alert = new Alert(AlertType.INFORMATION);
            alert.initOwner(primaryStage);
            alert.setHeaderText(null);
            alert.setContentText("All tasks have completed.");
            alert.show();
        }, tasks);

        ExecutorService executor = Executors.newFixedThreadPool(tasks.size(), r -> {
            // use daemon threads
            Thread thread = new Thread(r);
            thread.setDaemon(true);
            return thread;
        });
        tasks.forEach(executor::execute);
        executor.shutdown();
    }

    private static class MockTask extends Task<Void> {

        private final int iterations;

        MockTask(int iterations) {
            this.iterations = iterations;
        }

        @Override
        protected Void call() throws Exception {
            updateProgress(0, iterations);
            for (int i = 0; i < iterations; i++) {
                Thread.sleep(1L);
                updateProgress(i + 1, iterations);
            }
            return null;
        }

    }

}

The above reacts to the Worker#state property changing to a terminal state. There are other options as well. For instance, you could listen to the Worker#running property to change to false or add callbacks to the Task (e.g. Task#setOnSucceeded).

Slaw
  • 37,820
  • 8
  • 53
  • 80