javamultithreadingjavafxjavafx-8preemptive

Kill a Thread in the PropertyListener (JavaFX8)


I know that the actual model of Java is for collaborative Threads and that it forcing a Thread to die isn´t suppose to happens.

As Thread.stop() is deprecated (for the reasons listed above). I was trying to stop the thread through a BooleanProperty listener.

Here is the MCVE:

TestStopMethod.java

package javatest;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
public class TestStopMethod extends Thread {
    private BooleanProperty amIdead = new SimpleBooleanProperty(false);
    public void setDeath() {
        this.amIdead.set(true);
    }

    @Override
    public void run() {
        amIdead.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
            System.out.println("I'm dead!!!");
            throw new ThreadDeath();
        });
        for(;;);
    }
}

WatchDog.java

package javatest;

import java.util.TimerTask;

public class Watchdog extends TimerTask {
    TestStopMethod watched;
    public Watchdog(TestStopMethod target) {
        watched = target;
    }
    @Override
    public void run() {
        watched.setDeath();
        //watched.stop(); <- Works but this is exactly what I am trying to avoid
        System.out.println("You're dead!");
    }

}

Driver.java

package javatest;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Driver {

    public static void main(String[] args) {
        try {
            TestStopMethod mythread = new TestStopMethod();
            Timer t = new Timer();
            Watchdog w = new Watchdog(mythread);
            t.schedule(w, 1000);
            mythread.start();
            mythread.join();
            t.cancel();
            System.out.println("End of story");
        } catch (InterruptedException ex) {
            Logger.getLogger(Driver.class.getName()).log(Level.SEVERE, null, ex);
        }

    }
}

Solution

  • If you change a property value, the listeners are invoked on the same thread from which the property was changed (just think about how you would/could implement the property classes). So in your example the ThreadDeath error is thrown from the thread backing the Timer instance, which is not really what you intended.

    The proper way to terminate a thread externally (to that thread) is to set a flag, and then in the thread's implementation poll the flag at regular intervals. This is actually trickier than it sounds, because the flag has to be accessed from multiple threads, so access to it has to be properly synchronized.

    Fortunately, there are some utility classes to help with this. FutureTask, for example, wraps a Runnable or Callable and provides cancel() and isCancelled() methods. If you are using JavaFX, then the javafx.concurrent API provides some implementations of Callable and Runnable that do this, and also provide functionality specifically for executing code on the FX Application Thread. Have a look at the documentation for javafx.concurrent.Task for some examples.

    So, for example, you could do:

    package javatest;
    public class TestStopMethod implements Runnable {
    
        @Override
        public void run() {
            try {
                synchronized(this) {
                    for(;;) {
                        wait(1); 
                    }
                }
            } catch (InterruptedException exc) {
                System.out.println("Interrupted");
            }
        }
    }
    

    Watchdog.java:

    package javatest;
    
    import java.util.TimerTask;
    import java.util.concurrent.Future;
    
    public class Watchdog extends TimerTask {
        Future<Void> watched;
        public Watchdog(Future<Void> target) {
            watched = target;
        }
        @Override
        public void run() {
            watched.cancel(true);
            //watched.stop(); <- Works but this is exactly what I am trying to avoid
            System.out.println("You're dead!");
        }
    }
    

    Driver.java:

    package javatest;
    
    import java.util.*;
    import java.util.concurrent.FutureTask;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    public class Driver {
    
        public static void main(String[] args) {
            try {
                FutureTask<Void> myTask = new FutureTask<>(new TestStopMethod(), null);
                Timer t = new Timer();
                Watchdog w = new Watchdog(myTask);
                t.schedule(w, 1000);
                Thread mythread = new Thread(myTask);
                mythread.start();
                mythread.join();
                t.cancel();
                System.out.println("End of story");
            } catch (InterruptedException ex) {
                Logger.getLogger(Driver.class.getName()).log(Level.SEVERE, null, ex);
            }
    
        }
    }
    

    In a JavaFX application, you might do it like this. Note that things go bad if you try to do this without the FX Application Thread running, because the cancelled flag in an FX Task must be updated on that thread.

    package javatest;
    
    import javafx.concurrent.Task;
    
    public class TestStopMethod extends Task<Void> {
    
        @Override
        public Void call() {
            System.out.println("Calling");
            while (true) {
                if (isCancelled()) {
                    System.out.println("Cancelled");
                    break ;
                }
            }
            System.out.println("Exiting");
            return null ;
        }
    }
    

    Watchdog.java:

    package javatest;
    
    import java.util.TimerTask;
    
    import javafx.concurrent.Task;
    
    public class Watchdog extends TimerTask {
        Task<Void> watched;
        public Watchdog(Task<Void> target) {
            watched = target;
        }
        @Override
        public void run() {
            watched.cancel();
            //watched.stop(); <- Works but this is exactly what I am trying to avoid
            System.out.println("You're dead!");
        }
    
    }
    

    Driver.java

    package javatest;
    
    import java.util.Timer;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    import javafx.application.Application;
    import javafx.concurrent.Task;
    import javafx.concurrent.Worker;
    import javafx.scene.Scene;
    import javafx.scene.control.TextArea;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class Driver extends Application {
    
        @Override
        public void start(Stage primaryStage) {
            try {
    
                TextArea console = new TextArea();
                BorderPane root = new BorderPane(console);
                Scene scene = new Scene(root, 600, 400);
                primaryStage.setScene(scene);
                primaryStage.show();
    
                Task<Void> myTask = new TestStopMethod();
                Timer t = new Timer();
                Watchdog w = new Watchdog(myTask);
                t.schedule(w, 1000);
                Thread mythread = new Thread(myTask);
                mythread.setDaemon(true);
    
                myTask.stateProperty().addListener((obs, oldState, newState) -> {
                    console.appendText("State change "+oldState+" -> "+newState+"\n");
                    if (oldState == Worker.State.RUNNING) {
                        t.cancel();
                        console.appendText("End of Story\n");
                    }
                });
                mythread.start();
    
            } catch (Exception ex) {
                Logger.getLogger(Driver.class.getName()).log(Level.SEVERE, null, ex);
            }
    
        }
    }