Comment arrêter correctement le fil en Java?

276

J'ai besoin d'une solution pour arrêter correctement le thread en Java.

J'ai une IndexProcessorclasse qui implémente l'interface Runnable:

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);

    @Override
    public void run() {
        boolean run = true;
        while (run) {
            try {
                LOGGER.debug("Sleeping...");
                Thread.sleep((long) 15000);

                LOGGER.debug("Processing");
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
                run = false;
            }
        }

    }
}

Et j'ai une ServletContextListenerclasse qui démarre et arrête le fil:

public class SearchEngineContextListener implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchEngineContextListener.class);

    private Thread thread = null;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        thread = new Thread(new IndexProcessor());
        LOGGER.debug("Starting thread: " + thread);
        thread.start();
        LOGGER.debug("Background process successfully started.");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        LOGGER.debug("Stopping thread: " + thread);
        if (thread != null) {
            thread.interrupt();
            LOGGER.debug("Thread successfully stopped.");
        }
    }
}

Mais lorsque j'arrête Tomcat, j'obtiens l'exception dans ma classe IndexProcessor:

2012-06-09 17:04:50,671 [Thread-3] ERROR  IndexProcessor Exception
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at lt.ccl.searchengine.processor.IndexProcessor.run(IndexProcessor.java:22)
    at java.lang.Thread.run(Unknown Source)

J'utilise JDK 1.6. La question est donc:

Comment puis-je arrêter le fil et ne lever aucune exception?

PS Je ne veux pas utiliser la .stop();méthode car elle est obsolète.

Paulius Matulionis
la source
1
Terminer un thread à mi-chemin générera toujours une exception. Si c'est un comportement normal, vous pouvez simplement attraper et ignorer le InterruptedException. C'est ce que je pense, mais je me demande aussi comment est la voie standard.
nhahtdh
Je n'ai pas utilisé de threads très souvent, donc je suis assez nouveau dans les threads, donc je ne sais pas si c'est un comportement normal d'ignorer l'exception. C'est pourquoi je demande.
Paulius Matulionis
Dans de nombreux cas, il est normal d'ignorer l'exception et de terminer le traitement de la méthode. Voir ma réponse ci-dessous pour savoir pourquoi cela vaut mieux qu'une approche basée sur un drapeau.
Matt
1
Une explication soignée par B. Goetz concernant InterruptedExceptionpeut être trouvée à ibm.com/developerworks/library/j-jtp05236 .
Daniel
l'InterruptedException n'est pas un problème, votre seul problème dans le code publié est que vous ne devriez pas l'enregistrer comme une erreur, il n'y a vraiment pas de raison impérieuse de l'enregistrer comme tout sauf en tant que débogage juste pour démontrer que cela s'est produit au cas où vous seriez intéressé . la réponse sélectionnée est malheureuse car elle ne permet pas de couper les appels courts vers des appels tels que sommeil et attente.
Nathan Hughes

Réponses:

173

Dans la IndexProcessorclasse, vous avez besoin d'un moyen de définir un indicateur qui informe le thread qu'il devra se terminer, similaire à la variable runque vous avez utilisée uniquement dans la portée de la classe.

Lorsque vous souhaitez arrêter le thread, vous définissez ce drapeau et appelez join()le thread et attendez qu'il se termine.

Assurez-vous que l'indicateur est thread-safe en utilisant une variable volatile ou en utilisant des méthodes getter et setter qui sont synchronisées avec la variable utilisée comme indicateur.

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);
    private volatile boolean running = true;

    public void terminate() {
        running = false;
    }

    @Override
    public void run() {
        while (running) {
            try {
                LOGGER.debug("Sleeping...");
                Thread.sleep((long) 15000);

                LOGGER.debug("Processing");
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
                running = false;
            }
        }

    }
}

Puis dans SearchEngineContextListener:

public class SearchEngineContextListener implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchEngineContextListener.class);

    private Thread thread = null;
    private IndexProcessor runnable = null;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        runnable = new IndexProcessor();
        thread = new Thread(runnable);
        LOGGER.debug("Starting thread: " + thread);
        thread.start();
        LOGGER.debug("Background process successfully started.");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        LOGGER.debug("Stopping thread: " + thread);
        if (thread != null) {
            runnable.terminate();
            thread.join();
            LOGGER.debug("Thread successfully stopped.");
        }
    }
}
DrYap
la source
3
J'ai fait exactement la même chose que vous avez donné les exemples dans votre réponse juste avant de regarder que vous l'avez édité. Très bonne réponse! Merci, maintenant tout fonctionne parfaitement :)
Paulius Matulionis
1
Que se passe-t-il si la logique des threads est complexe et appelle de nombreuses méthodes d'autres classes? Il n'est pas possible de vérifier le drapeau booléen partout. Que faire alors?
Soteric
Vous devrez modifier la conception du code afin de le créer de manière à ce qu'un signal envoyé à Runnable entraîne la fermeture du thread. La plupart des utilisations ont cette boucle dans la méthode run, il n'y a donc généralement pas de problème.
DrYap
3
Que se passe-t-il si l'instruction join () lève une InterruptedException?
benzaita
14
A voté pour la diffusion de mauvais conseils. l'approche du drapeau roulé à la main signifie que l'application doit attendre la fin du sommeil, où une interruption réduirait le sommeil. Il serait facile de le modifier pour utiliser l'interruption du thread #.
Nathan Hughes,
298

L'utilisation Thread.interrupt()est une façon parfaitement acceptable de le faire. En fait, c'est probablement préférable à un drapeau comme suggéré ci-dessus. La raison en est que si vous êtes dans un appel de blocage interruptible (comme Thread.sleepou en utilisant des opérations de canal java.nio), vous pourrez en fait sortir immédiatement.

Si vous utilisez un indicateur, vous devez attendre la fin de l'opération de blocage, puis vous pouvez vérifier votre indicateur. Dans certains cas, vous devez quand même le faire, par exemple en utilisant standard InputStream/ OutputStreamqui ne sont pas interruptibles.

Dans ce cas, lorsqu'un thread est interrompu, il n'interrompra pas l'IO, cependant, vous pouvez facilement le faire régulièrement dans votre code (et vous devez le faire à des points stratégiques où vous pouvez arrêter et nettoyer en toute sécurité)

if (Thread.currentThread().isInterrupted()) {
  // cleanup and stop execution
  // for example a break in a loop
}

Comme je l'ai dit, le principal avantage Thread.interrupt()est que vous pouvez immédiatement interrompre les appels interruptibles, ce que vous ne pouvez pas faire avec l'approche du drapeau.

Mat
la source
32
+1 - Thread.interupt () est certainement préférable à l'implémentation de la même chose à l'aide d'un indicateur ad hoc.
Stephen C
2
Je pense aussi que c'est la manière parfaite et efficace de le faire. +1
RoboAlex
4
Il y a une petite faute de frappe dans le code, Thread.currentThread () n'a pas la parenthèse.
Vlad V
1
En fait, il n'est pas préférable d'utiliser un indicateur, car quelqu'un d'autre qui entre en contact avec le thread peut l'interrompre depuis un autre endroit, ce qui le fait s'arrêter et est très difficile à déboguer. Utilisez toujours un drapeau également.
JohnyTex
Dans ce cas spécifique, l'appel interrupt()peut être correct, mais dans de nombreux autres cas, il ne l'est pas (par exemple, si une ressource doit être fermée). Si quelqu'un change le fonctionnement interne de la boucle, vous devez vous rappeler de passer interrupt()à la méthode booléenne. J'irais pour le chemin sûr depuis le début et j'utiliserais le drapeau.
m0skit0
25

Réponse simple: vous pouvez arrêter un thread INTERNE de l'une des deux manières suivantes:

  • La méthode run atteint un sous-programme de retour.
  • La méthode Run se termine et renvoie implicitement.

Vous pouvez également arrêter les discussions EXTERNE:

  • Appelez system.exit(cela tue tout votre processus)
  • Appelez la interrupt()méthode de l'objet thread *
  • Voir si le thread a une méthode implémentée qui semble fonctionner (comme kill()ou stop())

*: L'attente est que cela est censé arrêter un thread. Cependant, ce que le thread fait réellement lorsque cela se produit dépend entièrement de ce que le développeur a écrit lorsqu'il a créé l'implémentation du thread.

Un modèle courant que vous voyez avec les implémentations de méthode d'exécution est un while(boolean){}, où le booléen est généralement quelque chose nommé isRunning, c'est une variable membre de sa classe de threads, il est volatile, et généralement accessible par d'autres threads par une méthode de définition de sortes, par exemple kill() { isRunnable=false; }. Ces sous-programmes sont agréables car ils permettent au thread de libérer toutes les ressources qu'il détient avant de se terminer.

hamsterofdark
la source
3
"Ces sous-programmes sont agréables car ils permettent au thread de libérer toutes les ressources qu'il détient avant de se terminer." Je ne comprends pas. Vous pouvez parfaitement nettoyer les ressources détenues d'un thread en utilisant le statut interrompu "officiel". Vérifiez-le simplement à l'aide de Thread.currentThread (). IsInterrupted () ou Thread.interrupted () (selon ce qui correspond à vos besoins), ou interceptez InterruptedException et nettoyez. Où est le problème?
Franz D.19
Je n'ai pas été en mesure de comprendre pourquoi la méthode de l'indicateur fonctionne, car je n'avais pas compris qu'elle s'arrête lorsque les résultats de l'exécution reviennent !!! C'était si simple, cher monsieur, merci d'avoir signalé cela, personne ne l'avait fait explicitement.
thahgr
9

Vous devez toujours terminer les threads en vérifiant un indicateur dans la run()boucle (le cas échéant).

Votre fil devrait ressembler à ceci:

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);
    private volatile boolean execute;

    @Override
    public void run() {
        this.execute = true;
        while (this.execute) {
            try {
                LOGGER.debug("Sleeping...");
                Thread.sleep((long) 15000);

                LOGGER.debug("Processing");
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
                this.execute = false;
            }
        }
    }

    public void stopExecuting() {
        this.execute = false;
    }
}

Ensuite, vous pouvez terminer le fil en appelant thread.stopExecuting(). De cette façon, le fil est terminé propre, mais cela prend jusqu'à 15 secondes (en raison de votre sommeil). Vous pouvez toujours appeler thread.interrupt () si c'est vraiment urgent - mais la manière préférée devrait toujours être de vérifier l'indicateur.

Pour éviter d'attendre 15 secondes, vous pouvez répartir le sommeil comme ceci:

        ...
        try {
            LOGGER.debug("Sleeping...");
            for (int i = 0; (i < 150) && this.execute; i++) {
                Thread.sleep((long) 100);
            }

            LOGGER.debug("Processing");
        } catch (InterruptedException e) {
        ...
Chris
la source
2
ce n'est pas un Thread- il implémente Runnable- vous ne pouvez pas appeler de Threadméthodes dessus sauf si vous le déclarez comme un Threadcas auquel vous ne pouvez pas appelerstopExecuting()
Don Cheadle
7

En règle générale, un thread se termine lorsqu'il est interrompu. Alors, pourquoi ne pas utiliser le booléen natif? Essayez isInterrupted ():

Thread t = new Thread(new Runnable(){
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                // do stuff         
            }   
        }});
    t.start();

    // Sleep a second, and then interrupt
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {}
    t.interrupt();

ref- Comment puis-je tuer un fil? sans utiliser stop ();

Lovekush Vishwakarma
la source
5

Pour synchroniser les threads, je préfère utiliser CountDownLatchce qui aide les threads à attendre la fin du processus. Dans ce cas, la classe de travail est configurée avec une CountDownLatchinstance avec un nombre donné. Un appel à la awaitméthode se bloquera jusqu'à ce que le nombre actuel atteigne zéro en raison des appels de la countDownméthode ou que le délai d'expiration soit atteint. Cette approche permet d'interrompre un thread instantanément sans avoir à attendre le temps d'attente spécifié:

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);

    private final CountDownLatch countdownlatch;
    public IndexProcessor(CountDownLatch countdownlatch) {
        this.countdownlatch = countdownlatch;
    }


    public void run() {
        try {
            while (!countdownlatch.await(15000, TimeUnit.MILLISECONDS)) {
                LOGGER.debug("Processing...");
            }
        } catch (InterruptedException e) {
            LOGGER.error("Exception", e);
            run = false;
        }

    }
}

Lorsque vous souhaitez terminer l'exécution de l'autre thread, exécutez countDown sur le CountDownLatchet joinle thread sur le thread principal:

public class SearchEngineContextListener implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchEngineContextListener.class);

    private Thread thread = null;
    private IndexProcessor runnable = null;
    private CountDownLatch countdownLatch = null;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        countdownLatch = new CountDownLatch(1);
        Thread thread = new Thread(new IndexProcessor(countdownLatch));
        LOGGER.debug("Starting thread: " + thread);
        thread.start();
        LOGGER.debug("Background process successfully started.");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        LOGGER.debug("Stopping thread: " + thread);
        if (countdownLatch != null) 
        {
            countdownLatch.countDown();
        } 
        if (thread != null) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
            }
            LOGGER.debug("Thread successfully stopped.");
        } 
    }
}
Eduardo Sanchez-Ros
la source
3

Quelques informations supplémentaires. Le drapeau et l'interruption sont suggérés dans le document Java.

https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

private volatile Thread blinker;

public void stop() {
    blinker = null;
}

public void run() {
    Thread thisThread = Thread.currentThread();
    while (blinker == thisThread) {
        try {
            Thread.sleep(interval);
        } catch (InterruptedException e){
        }
        repaint();
    }
}

Pour un thread qui attend pendant de longues périodes (par exemple, pour la saisie), utilisez Thread.interrupt

public void stop() {
     Thread moribund = waiter;
      waiter = null;
      moribund.interrupt();
 }
Feng
la source
3
N'ignorez jamais une exception InterruptedException. Cela signifie qu'un autre code demande explicitement à votre thread de se terminer. Un thread qui ignore cette demande est un thread non autorisé. La bonne façon de gérer une exception InterruptedException est de quitter la boucle.
VGR
2

Je n'ai pas réussi à faire fonctionner l'interruption sous Android, j'ai donc utilisé cette méthode, elle fonctionne parfaitement:

boolean shouldCheckUpdates = true;

private void startupCheckForUpdatesEveryFewSeconds() {
    threadCheckChat = new Thread(new CheckUpdates());
    threadCheckChat.start();
}

private class CheckUpdates implements Runnable{
    public void run() {
        while (shouldCheckUpdates){
            System.out.println("Do your thing here");
        }
    }
}

 public void stop(){
        shouldCheckUpdates = false;
 }
Mindborg
la source
Cela est susceptible d'échouer, car shouldCheckUpdates ne l'est pas volatile. Voir docs.oracle.com/javase/specs/jls/se9/html/jls-17.html#jls-17.3 .
VGR
0

Parfois j'essaierai 1000 fois dans mon onDestroy () / contextDestroyed ()

      @Override
    protected void onDestroy() {
        boolean retry = true;
        int counter = 0;
        while(retry && counter<1000)
        {
            counter++;
            try{thread.setRunnung(false);
                thread.join();
                retry = false;
                thread = null; //garbage can coll
            }catch(InterruptedException e){e.printStackTrace();}
        }

    }
Ebin Joy
la source