Search code examples
javauser-interfaceanimationjavafxconcurrency

"java.lang.IllegalStateException: Task must only be used from the FX Application Thread" despite task created and ran from the FX Application Thread


I have a single scene JavaFX app, that runs an animation, the animation needs to switch back and forth between two states.
Based on an example in "Java How to Program, 11/e, Early Objects" I have written a controller that creates a similar setup in the initialization method,
And a task with boolean value that signals the animation timer when to switch states by fliping its value and then sleep.
I keep getting "java.lang.IllegalStateException: Task must only be used from the FX Application Thread" thrown from the call() method no metter what I do.

here is a simplified version of the controller:

public class AnimationController {
    @FXML public AnimationTimer myAnimationTimerExtention;
    private ExecutorService executorService;

   public void initialize() {
        myAnimationTimerExtention.setState(false);
        TimeingTask task = new TimeingTask (Duration.ofSeconds(5));

        task .valueProperty().addListener((observableValue, oldValue, newValue) -> {
            myAnimationTimerExtention.setState(false);
        });

        executorService = Executors.newFixedThreadPool(1);
        executorService.execute(timeTrafficLightTask);
        executorService.shutdown();
    }

And here is my task:

public class TimeingTask  extends Task<Boolean> {
    private final Duration duration;

    public TimeingTask(Duration duration) {
        this.duration = duration;
        updateValue(true);
    }

    @Override
    protected Boolean call() throws Exception {
        while (!isCancelled()) {
            updateValue(!getValue());
            try {
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                updateMessage("timing task got interrupted");
            }

        }

        return true;
    }
}

I have tried so far:

  1. moveing the initialization to be done in a button's action
  2. removing the listener and the update of the value
  3. verifing with Thread.currentThread().getName() I'm on the application theard in the controller.
  4. chack if the task somehow get canceld to see if the issue relates to this javafx bug, it is not getting canceld.
  5. compiling and running the example I'm basing this on to see if this is realted to my IDE configuration or JDK version, the example worked.
  6. surrounding the updateValue() in a Platform.runLater() the excption is thrown on the Theard.sleep()

in all of the above scenarios the excption was thrown


Solution

  • The exception is being thrown because you can only directly access the value property from the FX Application Thread. You can call updateValue() from any thread, but you cannot call getValue() from a background thread. If you inspect your stack trace carefully, you should see something like:

    java.lang.IllegalStateException: Task must only be used from the FX Application Thread
      at javafx.concurrent.Task.checkThread()
      at javafx.concurrent.Task.getValue()
      at yourpackage.TimeingTask.call()
    

    meaning that the exception is thrown by the call to getValue() (not the call to updateValue()) in your call() method.

    The implementation of Task.getValue() is

    @Override public final V getValue() { checkThread(); return value.get(); }
    

    with

    private void checkThread() {
        if (started && !isFxApplicationThread()) {
            throw new IllegalStateException("Task must only be used from the FX Application Thread");
        }
    }
    

    And, indeed, the documentation explicitly states

    Because the Task is designed for use with JavaFX GUI applications, it ensures that every change to its public properties, as well as change notifications for state, errors, and for event handlers, all occur on the main JavaFX application thread. Accessing these properties from a background thread (including the call() method) will result in runtime exceptions being raised.

    (My emphasis.)

    So when you invoke getValue() from your call() method (which of course is run on the background thread), it throws the IllegalStateException.


    Threads are greatly overkill for what you're trying to do here anyway. As described here, you can greatly simplify this with a Timeline:

    public void initialize() {
        myAnimationTimerExtention.setState(false);
        KeyFrame frame = new KeyFrame(Duration.ofSeconds(5), e -> 
            myAnimationTimerExtention.setState(! myAnimationTimerExtention.getState()));
        Timeline timeline = new Timeline(frame);
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();
    
    }
    

    Or just build the functionality directly into your AnimationTimer, which could easily check the timestamp passed to the handle() method and flip the state if needed.

    This gets rid of the need for your Task subclass, the Executor, and means the application doesn't consume additional resources by creating an additional thread.


    Just in case the code in the question is a simplification, and you really need to use a background thread for some unknown reason, just shadow the value. To emphasize: do not add all the complexity of multithreading unless you need it. The solution above is a far better solution for the functionality you described.

    public class TimeTrafficLightTask  extends Task<Boolean> {
        private final Duration duration;
        private boolean state = true ;
    
        public TimeTrafficLightTask(Duration duration) {
            this.duration = duration;
            updateValue(state);
        }
    
        @Override
        protected Boolean call() throws Exception {
            while (!isCancelled()) {
                state = !state ;
                updateValue(state);
                try {
                    Thread.sleep(duration);
                } catch (InterruptedException e) {
                    updateMessage("timing task got interrupted");
                }
    
            }
    
            return true;
        }
    }