Search code examples
multithreadingjavafxsynchronizationtogglebutton

Is the operation of toggling a button in JavaFX atomic?


A toggle button in a JavaFX operation will be accessed by 2 separate threads.

1. One thread will be invoked as soon as user clicks (toggles button state) and will a) do something in the OS b) check if (a) succeeded c) exit on success / exit and return toggle button to previous state on failure

2 The other thread will monitor events asynchronous to the previous operation(s) and in case of a particular event it will change the button state.

Do I need to provide synchronization between threads 1 and 2 in terms of locking the button state?

EDIT: The idea proposed by James_D seems reasonable, but I just wanted to propose an alternative (whose effectiveness remains to be proved however). How about using synchronized code blocks, and using as lock the reference to the particular button, i.e. something like:

// getting the reference to the button 
@FXML
private ToggleButton tButtonToBeSynchronized

// Thread1
synchronized(tButtonToBeSynchronized) {
// do stuff with button upon user click 
}

// Thread2
synchronized(tButtonToBeSynchronized) {
// poll system every X seconds
// when asynchronous event occurs (not related to UI events)
// update tButtonToBeSynchronized state
}

Would that work in case these are called by different Controller classes? (assuming the reference to the tButtonToBeSynchronized is passed by reference - and not by value by the FXML framework?


Solution

  • Like most UI toolkits, JavaFX assumes a single threaded model. You should only ever access the state of nodes that are part of a scene graph from the FX Application Thread. So, toggling a button is not an atomic operation, and the code you describe is not guaranteed to work as you currently have it set up. In Java 8, it will likely throw a RuntimeException.

    JavaFX provides functionality to enable interoperability with background threads. The lowest level of these is Platform.runLater(Runnable r), which executes r on the FX Application Thread. So, your monitor thread (item 2 in your question) should change the state of the toggle button with

    Platform.runLater( () -> toggleButton.setSelected(...) );
    

    There is also a javafx.concurrent API. This provides a Task class, among others, which acts as both a Runnable and a java.util.concurrent.FutureTask, and additionally has a collection of callback methods for submitting code to be executed on the FX Application Thread at various points in the Task's lifecycle.

    So you should implement item 1 in your question as:

    ExecutorService exec = ... ; // e.g. Executors.newCachedThreadPool();
    
    toggleButton.selectedItemProperty().addListener((obs, wasSelected, isNowSelected) -> {
        if (isNowSelected) {
            Task<Void> task = new Task<Void>() {
                @Override
                public Void call() throws Exception {
                    // do something on OS
                    // throw exception if failed
                    return null ;
                }
            };
            task.setOnFailed(event -> toggleButton.setSelected(wasSelected));
            exec.submit(task);
        }
    });
    

    If you prefer to return a value indicating success or failure, you can do

    Task<Boolean> task = new Task<Boolean>() {
        @Override
        public Boolean call() {
            // do work...
            boolean successful =  ... ;
            return successful ;
        }
    };
    task.setOnSucceeded( event -> {
        boolean wasSuccessful = task.getValue();
        // ...
    });