The Task
and Service
classes are designed to encourage good practice and proper use of concurrency for some (but not all) common scenarios in GUI programming.
A typical scenario is that the application needs to execute some logic in response to a user action which may take a long time (maybe a long calculation, or, more commonly, a database lookup). The process will return a result which is then used to update the UI. As you know, the long-running process needs to be executed on a background thread to keep the UI responsive, and the update to the UI must be executed on the FX Application Thread.
The Task
class provides an abstraction for this kind of functionality, and represents a "one-off" task that is executed and produces a result. The call()
method will be executed on the background thread, and is designed to return the result of the process, and there are event listeners for when the task completes that are notified on the FX Application thread. The developer is strongly encouraged to initialize the Task
implementation with immutable state and have the call()
method return an immutable object, which guarantees proper synchronization between the background thread and the FX Application Thread.
There are additional common requirements on these kinds of tasks, such as updating a message or the progress as the task progresses. The application may also need to monitor the life-cycle state of the class (waiting to run, running, completed, failed with an exception, etc). Programming this correctly is quite subtly difficult, as it necessarily involves accessing mutable state in two different threads, and there are many application developers who are unaware of the subtleties. The Task
class provides simple hooks for this kind of functionality and takes care of all the synchronization.
To use this functionality, just create a Task
whose call()
method returns the result of your computation, register a handler for when the state transitions from RUNNING
to SUCCEEDED
, and run the task in a background thread:
final Task<MyDataType> task = new Task<MyDataType>() {
@Override
public MyDataType call() throws Exception {
// do work here...
return result ;
}
};
task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
MyDataType result = task.getValue(); // result of computation
// update UI with result
}
});
Thread t = new Thread(task);
t.setDaemon(true); // thread will not prevent application shutdown
t.start();
The way this works behind the scenes is that the Task
maintains a state
property, which is implemented using a regular JavaFX ObjectProperty
. The Task
itself is wrapped in a private implementation of Callable
, and the Callable
implementation is the object passed to the superclass constructor. Consequently, the Callable
's call()
method is actually the method executed in the background thread. The Callable
's call()
method is implemented as follows:
- Schedule a call on the FX Application thread (i.e. using
Platform.runLater()
) that updates the state
, first to SCHEDULED
, then to RUNNING
- Invoke the
call()
method of the Task
(i.e. the user-developed call()
method)
- Schedule a call on the FX Application Thread that updates the
value
property to the result of the call()
method
- Schedule a call on the FX Application Thread that updates the
state
property to SUCCEEDED
This last step will of course invoke listeners registered with the state
property, and since the state change was invoked on the FX Application Thread, so to will those listeners' handle()
methods.
For a full understanding of how this works, see the source code.
Commonly, the application may want to execute these tasks multiple discrete times, and monitor the current state representing all of the processes (i.e. "running" now means one instance is running, etc). The Service
class simply provides a wrapper for this via a createTask()
method. When the Service
is started, it gets a Task
instance by calling createTask()
, executes it via its Executor
, and transitions its own state accordingly.
There are of course many concurrency use cases that don't fit (at least cleanly) into the Task
or Service
implementations. If you have a single background Thread
that is running for the entire duration of your application (so it represents a continuous process, rather than a one-off task), then the Task
class is not a good fit. Examples of this might include a game loop, or (perhaps) polling. In these cases you may well be better off using your own Thread
with Platform.runLater()
to update the UI, but of course you have to handle proper synchronization of any variables that may be accessed by both threads. In my experience, it is worth spending some time thinking about whether these requirements can be re-organized into something that does fit into the Task
or Service
model, as if this can be done the resulting code structure is often much cleaner and easier to manage. There are certainly cases where this is not the case, however, in which case using a Thread
and Platform.runLater()
is appropriate.
One last comment on polling (or any other requirement for a periodically-scheduled background task). The Service
class looks like a good candidate for this, but it turns out to be quite hard to manage the periodicity effectively. JavaFX 8 introduced a ScheduledService
class which takes care of this functionality quite nicely, and also adds handling for cases such as repeated failure of the background task.