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.


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

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.


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.


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) {
        ...

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 ();


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

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

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

É 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();}
        }

    }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.