I was working on my JavaFX application when i noticed that on ScheduledService stopped being scheduled after a couple of runs.
I was not able to find any obvious reason for this. When i tracked that state of the ScheduledService it seemed that it switched to the the state SCHEDULED and then went silent. I reduced my code to almost nothing in the hope to narrow down the issue. I discovered that when i ScheduledService creates a Task of an anonymos class, the issue does not occur but when i use sub class or top level class it does.
package application;
import javafx.application.Application;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.stage.Stage;
public class MyApplication extends Application
{
private static volatile int counter;
@Override
public void start( Stage primaryStage )
{
ScheduledService<Void> svc = new ScheduledService<>()
{
@Override
protected Task<Void> createTask()
{
return new MyTask();// if i use new Task<Void>{...} it works fine
}
};
svc.start();
}
private static class MyTask extends Task<Void>
{
@Override
protected Void call() throws Exception
{
System.out.println( "MyTask#call() " + counter++ );
return null;
}
}
public static void main( String[] args )
{
launch( args );
}
}
The output
MyTask#call() 0
MyTask#call() 1
MyTask#call() 2
MyTask#call() 3
MyTask#call() 4
MyTask#call() 5
MyTask#call() 6
MyTask#call() 7
MyTask#call() 8
MyTask#call() 9
MyTask#call() 10
MyTask#call() 11
MyTask#call() 12
MyTask#call() 13
MyTask#call() 14
Is this a bug?
I'm Using
Windows 10
OracleOpen_jdk-12.0.1
javafx-sdk-12.0.1
I can reproduce the problem using the following environment:
Modifying the code slightly shows you what the problem is:
import java.lang.ref.Cleaner;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.stage.Stage;
public class Main extends Application {
private static final Cleaner CLEANER = Cleaner.create();
private static final AtomicInteger COUNTER = new AtomicInteger();
@Override
public void start(Stage primaryStage) {
var service = new ScheduledService<Void>() {
@Override
protected Task<Void> createTask() {
return new MyTask();
}
};
CLEANER.register(service, () -> {
System.out.println("SERVICE GARBAGE COLLECTED!");
System.exit(0);
});
service.start();
}
private static class MyTask extends Task<Void> {
@Override
protected Void call() throws Exception{
System.out.println("MyTask#call() " + COUNTER.incrementAndGet());
return null;
}
}
}
The above outputs:
MyTask#call() 1
MyTask#call() 2
MyTask#call() 3
MyTask#call() 4
MyTask#call() 5
MyTask#call() 6
MyTask#call() 7
MyTask#call() 8
MyTask#call() 9
MyTask#call() 10
MyTask#call() 11
MyTask#call() 12
MyTask#call() 13
MyTask#call() 14
MyTask#call() 15
MyTask#call() 16
MyTask#call() 17
MyTask#call() 18
MyTask#call() 19
SERVICE GARBAGE COLLECTED!
So that explains what is happening—the ScheduledService
instance is being garbage collected. This makes sense since you don't maintain a strong reference to the ScheduledService
instance and neither does the Task
instance being executed. Once the ScheduledService
instance is garbage collected it can't schedule another Task
for the next execution cycle.
I'm still a bit confused why it works with a anonymous class then. I mean in my example the class MyTask is static but even when it is not it does not work. Objects of a non-static inner class should have a reference to there outer class objects.
It's true that a non-static nested or anonymous class maintains a reference to an instance of the enclosing class. However, the enclosing class is Main
(MyApplication
in your code), not the anonymous ScheduledService
class.
return new MyTask();// if i use new Task<Void>{...} it works fine
The reason using new Task<Void>() { ... }
works is because now the enclosing instance is the ScheduledService
instance.
The ScheduledService
class uses an internal java.util.Timer
instance for scheduling the Task
for later execution. So while waiting to be executed the Task
is strongly reachable by the thread used by the Timer
. While being executed, the Task
is strongly reachable by the thread executing it (the thread comes from the Executor
used with in ScheduledService#executor
property).
So, when using an anonymous Task
enclosed by the anonymous ScheduledService
, the ScheduledService
is ultimately strongly reachable.
Curiously, if I add code to display the primary Stage
the ScheduledService
is never garbage collected. I'm not sure why this is the case. This is true even when I use Platform.setImplicitExit(false)
and close the Stage
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With