Como parar corretamente o Thread em Java?

276

Preciso de uma solução para parar corretamente o encadeamento em Java.

Eu tenho IndexProcessorclasse que implementa a 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;
            }
        }

    }
}

E eu tenho ServletContextListenerclasse que inicia e pára o segmento:

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

Mas quando eu desligo o tomcat, recebo a exceção na minha 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)

Estou usando o JDK 1.6. Então a questão é:

Como posso parar o thread e não lançar nenhuma exceção?

PS: Eu não quero usar o .stop();método porque está obsoleto.

Paulius Matulionis
fonte
1
Terminar um encadeamento no meio do caminho sempre gerará uma exceção. Se é um comportamento normal, você pode simplesmente pegar e ignorar o InterruptedException. É isso que penso, mas também me pergunto como é o caminho padrão.
Nhahtdh
Eu não uso os threads com muita frequência, por isso sou bastante novo nos threads, portanto não sei se é um comportamento normal ignorar a exceção. É por isso que estou perguntando.
Paulius Matulionis
Em muitos casos, é um comportamento normal ignorar a exceção e encerrar o processamento do método. Veja minha resposta abaixo para saber por que isso é melhor do que uma abordagem baseada em bandeiras.
Matt
1
Uma explicação clara de B. Goetz a respeito InterruptedExceptionpode ser encontrada em ibm.com/developerworks/library/j-jtp05236 .
Daniel
a InterruptedException não é um problema, seu único problema no código publicado é que você não deve registrá-lo como um erro, não há realmente um motivo convincente para registrá-lo como tudo, exceto como depuração apenas para demonstrar que isso aconteceu caso você esteja interessado . a resposta selecionada é lamentável, pois não permite cortar chamadas curtas para chamadas como suspensão e espera.
Nathan Hughes

Respostas:

173

Na IndexProcessorclasse, você precisa definir um sinalizador que informe ao thread que ele precisará finalizar, semelhante à variável runque você usou apenas no escopo da classe.

Quando você deseja interromper o encadeamento, defina esse sinalizador, chame join()o encadeamento e aguarde o término.

Verifique se o sinalizador é seguro para threads usando uma variável volátil ou métodos getter e setter que são sincronizados com a variável que está sendo usada como sinalizador.

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

    }
}

Então em 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
fonte
3
Fiz exatamente o mesmo que você deu nos exemplos da sua resposta, pouco antes de parecer que você o editou. Ótima resposta! Obrigado, agora tudo funciona perfeitamente :)
Paulius Matulionis
1
E se a lógica do encadeamento for complexa e invocar muitos métodos de outras classes? Não é possível verificar o sinalizador booleano em todos os lugares. O que fazer então?
Sotérico
Você precisaria alterar o design do código para construí-lo de maneira que um sinal para o Runnable faça com que o thread saia. A maioria dos usos possui esse loop no método run, portanto, geralmente não há o problema.
DrYap
3
O que acontece se a instrução join () lança uma InterruptedException?
benzaita
14
Voto por espalhar maus conselhos. a abordagem de sinalização rolada à mão significa que o aplicativo precisa esperar o sono terminar, onde a interrupção reduziria o sono. Seria fácil modificar isso para usar a interrupção do thread #.
91915 Nathan Hughes
298

Usar Thread.interrupt()é uma maneira perfeitamente aceitável de fazer isso. De fato, provavelmente é preferível a uma bandeira, como sugerido acima. O motivo é que, se você estiver em uma chamada de bloqueio interruptível (como Thread.sleepou usando as operações do canal java.nio), poderá realmente romper essas chamadas imediatamente.

Se você usar um sinalizador, precisará aguardar o término da operação de bloqueio e poderá verificar seu sinalizador. Em alguns casos, você precisa fazer isso de qualquer maneira, como usar o padrão InputStream/ OutputStreamque não é interrompível.

Nesse caso, quando um encadeamento é interrompido, ele não interrompe a IO; no entanto, você pode fazer isso rotineiramente com facilidade em seu código (e deve fazê-lo em pontos estratégicos onde pode parar e limpar com segurança)

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

Como eu disse, a principal vantagem Thread.interrupt()é que você pode interromper imediatamente as chamadas interrompíveis, o que não é possível com a abordagem de sinalização.

Matt
fonte
32
+1 - Thread.interupt () é definitivamente preferível à implementação da mesma coisa usando um sinalizador ad-hoc.
Stephen C
2
Eu também acho que essa é a maneira perfeita e eficiente de fazer isso. 1
RoboAlex
4
Há um pequeno erro de digitação no código, Thread.currentThread () não tem parênteses.
Vlad V
1
Na verdade, não é preferível usar um sinalizador porque alguém que entre em contato com o encadeamento pode interrompê-lo de outro lugar, fazendo com que ele pare e seja muito difícil depurar. Sempre use uma bandeira também.
precisa saber é o seguinte
Nesse caso específico, a chamada interrupt()pode ser aceitável, mas em muitos outros casos não é (por exemplo, se um recurso precisar ser fechado). Se alguém alterar o funcionamento interno do loop, você precisará se lembrar de mudar interrupt()para o modo booleano. Eu iria para o caminho seguro desde o início e usaria a bandeira.
M0skit0 23/09/16
25

Resposta simples: você pode interromper um tópico INTERNO de uma das duas maneiras mais comuns:

  • O método run atinge uma sub-rotina de retorno.
  • O método Run termina e retorna implicitamente.

Você também pode parar os threads EXTERNAMENTE:

  • Chamada system.exit(isso mata todo o processo)
  • Chame o interrupt()método do objeto de encadeamento *
  • Veja se o encadeamento possui um método implementado que parece que funcionaria (como kill()ou stop())

*: A expectativa é que isso pare um encadeamento. No entanto, o que o encadeamento realmente faz quando isso acontece depende inteiramente do que o desenvolvedor escreveu quando criou a implementação do encadeamento.

Um padrão comum que você vê nas implementações de métodos de execução é a while(boolean){}, onde o booleano é geralmente algo chamado isRunning, é uma variável membro de sua classe de encadeamentos, é volátil e geralmente acessível por outros encadeamentos por um método setter, por exemplo kill() { isRunnable=false; }. Essas sub-rotinas são boas porque permitem que o encadeamento libere todos os recursos que ele contém antes de terminar.

hamsterofdark
fonte
3
"Essas sub-rotinas são boas porque permitem que o thread libere todos os recursos que ele possui antes de terminar". Eu não entendo Você pode perfeitamente perfeitamente limpar os recursos retidos de um encadeamento usando o status interrompido "oficial". Basta verificar usando Thread.currentThread (). IsInterrupted () ou Thread.interrupted () (o que for mais adequado às suas necessidades) ou pegue a InterruptedException e a limpeza. Onde está o problema?
Franz D.
Não consegui entender por que o método flag funciona, porque eu não tinha entendido que ele para quando as execuções retornam !!! Isso foi tão simples, caro senhor, obrigado por apontar isso, ninguém fez isso explicitamente.
thahgr
9

Você sempre deve encerrar os threads verificando um sinalizador no run()loop (se houver).

Seu tópico deve ficar assim:

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

Em seguida, você pode encerrar o segmento chamando thread.stopExecuting(). Dessa forma, o encadeamento termina limpo, mas isso leva até 15 segundos (devido ao seu sono). Você ainda pode chamar thread.interrupt () se for realmente urgente - mas a maneira preferida deve sempre estar verificando o sinalizador.

Para evitar esperar 15 segundos, você pode dividir o sono da seguinte maneira:

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

            LOGGER.debug("Processing");
        } catch (InterruptedException e) {
        ...
Chris
fonte
2
não é um Thread- implementa Runnable- você não pode chamar Threadmétodos nele, a menos que o declare como um Threadcaso em que não pode chamarstopExecuting()
Don Cheadle
7

Normalmente, um encadeamento é encerrado quando é interrompido. Então, por que não usar o booleano nativo? Tente 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- Como posso matar um tópico? sem usar stop ();

Lovekush Vishwakarma
fonte
5

Para sincronizar threads, prefiro usar o CountDownLatchque ajuda os threads a esperar até que o processo esteja sendo concluído. Nesse caso, a classe de trabalhador é configurada com uma CountDownLatchinstância com uma determinada contagem. Uma chamada para o awaitmétodo será bloqueada até que a contagem atual atinja zero devido a invocações do countDownmétodo ou o tempo limite definido seja atingido. Essa abordagem permite interromper um encadeamento instantaneamente sem precisar esperar o tempo 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;
        }

    }
}

Quando você deseja concluir a execução do outro encadeamento, execute countDown no encadeamento CountDownLatche joinno encadeamento 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
fonte
3

Algumas informações adicionais. O sinalizador e a interrupção são sugeridos no documento 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 um encadeamento que aguarda longos períodos (por exemplo, entrada), use Thread.interrupt

public void stop() {
     Thread moribund = waiter;
      waiter = null;
      moribund.interrupt();
 }
Feng
fonte
3
Nunca ignore uma InterruptedException. Isso significa que algum outro código está solicitando explicitamente que seu encadeamento seja encerrado. Um segmento que ignora essa solicitação é um segmento não autorizado. A maneira correta de lidar com uma InterruptedException é sair do loop.
VGR 21/03
2

Como a interrupção não funcionou no Android, usei esse método, funciona perfeitamente:

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
fonte
É provável que isso falhe, porque shouldCheckUpdates não é volatile. Consulte docs.oracle.com/javase/specs/jls/se9/html/jls-17.html#jls-17.3 .
VGR 21/03
0

Em algum momento tentarei 1000 vezes no meu 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
fonte