Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Complex concurrency in JavaFX: using ObservableLists and Properties from multiple worker threads

I have multiple worker threads, and a JavaFX GUI which is reporting on what is happening in these threads.

There is a lot of data shared between threads and it needs to be visualized. So I'm using ObservableList's and Property's to be able to easily show the data in JavaFX.

I've made a small example app to show something similar to what happens in my application. It has 2 lists and the worker thread moves data from one list to the other. A status String is kept up to date. Full example code can be found at http://codetidy.com/6569/ (this code will crash, see later)

Here are the shared ObservableList's & Properties:

private ObservableList<String> newItems;
private ObservableList<String> readyItems;
private StringProperty status;

Here's how they are used in JavaFX:

listViewA.setItems(processor.getNewItems());
listViewB.setItems(processor.getReadyItems());
statusLabel.textProperty().bind(processor.getStatus());

The worker thread updates these lists and properties, but off course, it needs to do this on the JavaFX thread, and that's where things get ugly. This would be the code if I didn't have to update on the JavaFX thread:

  Runnable newItemAdder = new Runnable() {
      @Override
      public void run() {
          while(true) {
              synchronized (newItems) {
                  String newItem = checkForNewItem(); //slow
                  if (newItem != null) {
                      newItems.add(newItem);
                      newItems.notify();
                  }
                  if (newItems.size() >= 5)
                      status.set("Overload");
                  else
                      status.set("OK");
              }

              synchronized (readyItems) {
                  if (readyItems.size() > 10)
                      readyItems.remove(0);
              }

              try { Thread.sleep(200); } catch (InterruptedException e) { return; }
          }
      }
  };
  new Thread(newItemAdder).start();

  Runnable worker = new Runnable() {
      @Override
      public void run() {
          while(true) {
              List<String> toProcess = new ArrayList<String>();
              synchronized (newItems) {
                  if (newItems.isEmpty())
                      try { newItems.wait(); } catch (InterruptedException e) { return; }
                  toProcess.addAll(newItems);
              }

              for (String item : toProcess) {
                  String processedItem = processItem(item); //slow
                  synchronized (readyItems) {
                      readyItems.add(processedItem);
                  }
              }
          }
      }
  };
  new Thread(worker).start();

Off course, some things are easy to solve with Platform.runLater:

 Platform.runLater(new Runnable() {
     @Override
     public void run() {
         synchronized (newItems) {
             if (newItems.size() >= 5)
                 status.set("Overload");
             else
                 status.set("OK");
         }
     }
 });

That's fine for properties/lists that I only write to in the task, and only read in the JavaFX GUI. But it gets very complicated to do it for the lists in this example, on which you need to synchronize, read and write. You need add a lot of Platform.runLater and you need to block until the "runLater" task has finished. This results in very complex and hard to read and write code (I managed to get this example running this way, see what I mean: http://codetidy.com/6570/).

Are there any other ways to get my example working? I'd appreciate any other solution or partial solutions...

like image 744
Coder Nr 23 Avatar asked Aug 30 '13 10:08

Coder Nr 23


2 Answers

Background Info

Task javadoc includes numerous concurrency usage patterns for passing data between threads in JavaFX.

Task includes convenience data transfer methods such as updateMessage and can be used instead of your Runnable with the user defined status property.

When appropriate, consider using a collection structure designed for concurrency, such as a BlockingQueue. An additional advantage is that BlockingQueues can have size limits, which seems like something you want.

Some general advice

  1. Be very careful when using mutable observable items in multiple threads. It is easy to inadvertently trigger updates that result in race conditions, updates to the active scene graph off the application thread and other threading issues.
  2. As much as possible, use immutable data rather than mutable observable items.
  3. Leverage some of the higher level utilities from the JavaFX concurrency and java.util.concurrent libraries.
  4. Avoid explicit synchronization and notify statements as much as possible.
  5. Be careful placing synchronization or other potentially blocking statements in code that runs on the JavaFX Application Thread - as you may make your GUI unresponsive.
  6. Use the JavaFX concurrency utilities only when you need interaction with the JavaFX thread.
  7. Do a lot of your very complicated multi-threaded processing off of the JavaFX thread using standard Java concurrency utilities. Have a single co-ordinating JavaFX Task for coalescing and controlling UI feedback.

The above are just rules of thumb and don't need to be followed didactically.

Reasonably Complex Threading Samples

  • A chart renderer that demonstrates some of the principles above to render 300 charts while still keeping the UI responsive to progress updates and user interaction.
like image 90
jewelsea Avatar answered Nov 07 '22 07:11

jewelsea


The original links with the full example and the example solution by jewelsea are dead, so I'll add an answer with a short summary of what I ended up doing.

To make this easy, let's assume you start with 1 class that holds your data model (let's call it DataModel). An instance of this class is used by multiple threads that change it.

Now the problem is that you want to use the data model in javaFX, but you cannot simply change your data model to use Property and ObservableList etc. If you do that, the listeners will be called from non-javafx threads, and GUI's bound to them will throw exceptions.

Instead you need to make a seperate data model class for javaFX. This is simply the JavaFX version of the original one (let's call it FXDataModel). So this version contains the same info, but it uses javaFX Property and ObservableList. This way, you can bind your GUI to it.

The next step is to periodically update a FXDataModel instance using the DataModel instance. To do this, you add an update(DataModel dataModel) method to FXDataModel, which copies the data from the original data model into the FXDataModel instance. This update function must always be called on the javaFX thread. Finally, all you need to do is periodically call that update function.

In my real scenario, I call the update function every 200 ms, and that is more than enough to be able to show a live view of the data model in the GUI. (Things get more complex if you want to more than a view of the data model, and you want to change things from the GUI, but that is not something I needed to do)

like image 30
Coder Nr 23 Avatar answered Nov 07 '22 08:11

Coder Nr 23