¿Cómo detener correctamente el hilo en Java?

276

Necesito una solución para detener correctamente el hilo en Java.

Tengo una IndexProcessorclase que implementa la interfaz 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;
            }
        }

    }
}

Y tengo una ServletContextListenerclase que comienza y detiene el hilo:

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.");
        }
    }
}

Pero cuando apago tomcat, obtengo la excepción en mi clase 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)

Estoy usando JDK 1.6. Entonces la pregunta es:

¿Cómo puedo detener el hilo y no lanzar ninguna excepción?

PD : No quiero usar el .stop();método porque está en desuso.

Paulius Matulionis
fuente
1
Terminar un subproceso a mitad de camino siempre generará una excepción. Si se trata de un comportamiento normal, puede capturar e ignorar el InterruptedException. Esto es lo que pienso, pero también me pregunto cómo es la forma estándar.
nhahtdh
No he estado usando hilos muy a menudo, así que soy bastante nuevo en los hilos, por lo que no sé si es un comportamiento normal ignorar la excepción. Por eso estoy preguntando.
Paulius Matulionis
En muchos casos, es un comportamiento normal ignorar la excepción y finalizar el procesamiento del método. Vea mi respuesta a continuación para saber por qué esto es mejor que un enfoque basado en la bandera.
Matt
1
Una explicación ordenada por B. Goetz En cuanto InterruptedExceptionse puede encontrar en ibm.com/developerworks/library/j-jtp05236 .
Daniel
InterruptionException no es un problema, su único problema en el código publicado es que no debe registrarlo como un error, realmente no hay una razón convincente para registrarlo como todo excepto como depuración solo para demostrar que sucedió en caso de que esté interesado . la respuesta seleccionada es desafortunada porque no permite cortar llamadas cortas a llamadas como dormir y esperar.
Nathan Hughes

Respuestas:

173

En la IndexProcessorclase, necesita una forma de establecer un indicador que informe al subproceso que tendrá que terminar, similar a la variable runque ha utilizado solo en el ámbito de la clase.

Cuando desee detener el hilo, establezca esta bandera y llame join()al hilo y espere a que termine.

Asegúrese de que el indicador sea seguro para subprocesos utilizando una variable volátil o utilizando métodos getter y setter que estén sincronizados con la variable que se utiliza como indicador.

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;
            }
        }

    }
}

Luego en 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
fuente
3
Hice exactamente lo mismo que usted dio los ejemplos en su respuesta justo antes de que pareciera que lo editó. ¡Gran respuesta! Gracias, ahora todo funciona perfectamente :)
Paulius Matulionis
1
¿Qué pasa si la lógica del hilo es compleja e invoca muchos métodos de otras clases? No es posible verificar la bandera booleana en todas partes. ¿Qué hacer entonces?
Soteric
Tendría que cambiar el diseño del código para que lo construya de manera que una señal al Runnable haga que el hilo salga. La mayoría de los usos tienen este bucle en el método de ejecución, por lo que generalmente no existe el problema.
DrYap
3
¿Qué sucede si la instrucción join () arroja una excepción interrumpida?
benzaita
14
Votados por difundir malos consejos. El enfoque de bandera enrollada a mano significa que la aplicación tiene que esperar a que termine el sueño, donde la interrupción acortaría el sueño. Sería fácil modificar esto para usar Thread # interrupt.
Nathan Hughes
298

Usar Thread.interrupt()es una forma perfectamente aceptable de hacer esto. De hecho, probablemente sea preferible a una bandera como se sugirió anteriormente. La razón es que si está en una llamada de bloqueo interrumpible (como Thread.sleepo usando las operaciones del canal java.nio), en realidad podrá salir de ellas de inmediato.

Si usa una bandera, debe esperar a que finalice la operación de bloqueo y luego puede verificar su bandera. En algunos casos, debe hacer esto de todos modos, como el uso de estándar InputStream/ OutputStreamque no son interrumpibles.

En ese caso, cuando se interrumpe un subproceso, no interrumpirá el IO, sin embargo, puede hacerlo fácilmente de forma rutinaria en su código (y debe hacerlo en puntos estratégicos donde puede detenerse y limpiar de forma segura)

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

Como dije, la principal ventaja Thread.interrupt()es que puede salir inmediatamente de las llamadas interrumpibles, lo que no puede hacer con el enfoque de bandera.

Mate
fuente
32
+1: Thread.interupt () es definitivamente preferible a implementar lo mismo usando un indicador ad-hoc.
Stephen C
2
También creo que esta es la manera perfecta y eficiente de hacerlo. +1
RoboAlex
44
Hay un pequeño error tipográfico en el código, Thread.currentThread () no tiene paréntesis.
Vlad V
1
En realidad, no es preferible usar una bandera porque alguien más que entre en contacto con el hilo puede interrumpirlo desde otro lugar, haciendo que se detenga y que sea muy difícil de depurar. Siempre use una bandera también.
JohnyTex
En este caso específico, las llamadas interrupt()pueden estar bien, pero en muchos otros casos no lo está (por ejemplo, si un recurso necesita ser cerrado). Si alguien cambia el funcionamiento interno del bucle, deberá recordar cambiar interrupt()a la forma booleana. Yo iría por el camino seguro desde el principio y usaría la bandera.
m0skit0
25

Respuesta simple: puede detener un hilo INTERNO de una de dos maneras comunes:

  • El método de ejecución alcanza una subrutina de retorno.
  • El método de ejecución finaliza y regresa implícitamente.

También puede detener hilos EXTERNAMENTE:

  • Llamada system.exit(esto mata todo el proceso)
  • Llame al interrupt()método del objeto de hilo *
  • Vea si el hilo tiene un método implementado que parece que funcionaría (como kill()o stop())

*: La expectativa es que se supone que esto detendrá un hilo. Sin embargo, lo que el hilo hace realmente cuando esto sucede depende completamente de lo que el desarrollador escribió cuando crearon la implementación del hilo.

Un patrón común que se ve con las implementaciones de métodos de ejecución es un while(boolean){}, donde el booleano suele tener un nombre isRunning, es una variable miembro de su clase de subproceso, es volátil y, por lo general, otros subprocesos pueden acceder mediante un método de establecimiento kill() { isRunnable=false; }. Estas subrutinas son buenas porque permiten que el hilo libere cualquier recurso que tenga antes de terminar.

hamsterofdark
fuente
3
"Estas subrutinas son buenas porque permiten que el hilo libere cualquier recurso que tenga antes de terminar". No entiendo. Puede limpiar perfectamente los recursos retenidos de un hilo utilizando el estado interrumpido "oficial". Simplemente verifíquelo usando Thread.currentThread (). IsInterrupted () o Thread.interrupted () (lo que se ajuste a su necesidad), o capture la InterruptedException y limpie. ¿Dónde está el problema?
Franz D.
¡No pude entender por qué funciona el método de marca, porque no había entendido que se detiene cuando vuelven los golpes de ejecución! Esto fue tan simple, querido señor, gracias por señalar esto, nadie lo había hecho explícitamente.
thahgr
9

Siempre debe finalizar los hilos marcando una bandera en el run()bucle (si lo hay).

Su hilo debería verse así:

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;
    }
}

Luego puede finalizar el hilo llamando thread.stopExecuting(). De esa manera, el hilo termina limpio, pero esto toma hasta 15 segundos (debido a su sueño). Todavía puede llamar a thread.interrupt () si es realmente urgente, pero la forma preferida siempre debe ser verificar la bandera.

Para evitar esperar 15 segundos, puede dividir el sueño de esta manera:

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

            LOGGER.debug("Processing");
        } catch (InterruptedException e) {
        ...
Chris
fuente
2
no es un Thread- implementa Runnable- no puedes llamar Threadmétodos a menos que lo declares como Threaden cuyo caso no puedes llamarstopExecuting()
Don Cheadle
7

Por lo general, un subproceso se termina cuando se interrumpe. Entonces, ¿por qué no usar el booleano nativo? Prueba 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- ¿Cómo puedo matar un hilo? sin usar stop ();

Lovekush Vishwakarma
fuente
5

Para sincronizar hilos prefiero usar CountDownLatchque ayuda a los hilos a esperar hasta que se complete el proceso. En este caso, la clase de trabajo se configura con una CountDownLatchinstancia con un recuento determinado. Una llamada al awaitmétodo se bloqueará hasta que el recuento actual llegue a cero debido a invocaciones del countDownmétodo o se alcance el tiempo de espera establecido. Este enfoque permite interrumpir un hilo instantáneamente sin tener que esperar a que transcurra el tiempo de espera especificado:

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;
        }

    }
}

Cuando desee finalizar la ejecución del otro hilo, ejecute countDown en el hilo CountDownLatchy joinal hilo 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
fuente
3

Alguna información complementaria. Tanto el indicador como la interrupción se sugieren en el documento de 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();
    }
}

Para un hilo que espera por largos períodos (por ejemplo, para entrada), use Thread.interrupt

public void stop() {
     Thread moribund = waiter;
      waiter = null;
      moribund.interrupt();
 }
Feng
fuente
3
Nunca ignore una excepción interrumpida. Significa que algún otro código está pidiendo explícitamente que su hilo termine. Un hilo que ignora esa solicitud es un hilo falso. La forma correcta de manejar una excepción interrumpida es salir del bucle.
VGR
2

No conseguí que la interrupción funcionara en Android, así que utilicé este método, funciona perfectamente:

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
fuente
Es probable que esto falle, porque shouldCheckUpdates no lo es volatile. Ver docs.oracle.com/javase/specs/jls/se9/html/jls-17.html#jls-17.3 .
VGR
0

En algún momento lo intentaré 1000 veces en mi 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
fuente