Jak poprawnie zatrzymać wątek w Javie?


276

Potrzebuję rozwiązania, aby poprawnie zatrzymać wątek w Javie.

Mam IndexProcessorklasę, która implementuje interfejs 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;
            }
        }

    }
}

I mam ServletContextListenerklasę, która rozpoczyna i zatrzymuje wątek:

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

Ale kiedy zamykam tomcat, otrzymuję wyjątek w mojej klasie 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)

Używam JDK 1.6. Pytanie brzmi:

Jak mogę zatrzymać wątek i nie zgłaszać żadnych wyjątków?

PS Nie chcę używać .stop();metody, ponieważ jest przestarzała.


1
Zakończenie wątku w połowie drogi zawsze wygeneruje wyjątek. Jeśli jest to normalne zachowanie, możesz po prostu złapać i zignorować InterruptedException. Tak myślę, ale zastanawiam się też, jak to jest w standardowej formie.
nhahtdh

Nie używam często wątków, więc jestem całkiem nowy w wątkach, więc nie wiem, czy ignorowanie wyjątku jest normalnym zachowaniem. Właśnie dlatego pytam.
Paulius Matulionis

W wielu przypadkach normalnym zachowaniem jest ignorowanie wyjątku i zakończenie przetwarzania metody. Zobacz moją odpowiedź poniżej, dlaczego jest to lepsze niż podejście oparte na flagach.
Matt

1
Dobre wyjaśnienie B. Goetza dotyczące InterruptedExceptionmożna znaleźć na stronie ibm.com/developerworks/library/j-jtp05236 .
Daniel

InterruptedException nie stanowi problemu, jedynym problemem w opublikowanym kodzie jest to, że nie powinieneś rejestrować go jako błędu, naprawdę nie ma istotnego powodu, aby logować go jako wszystko, z wyjątkiem debugowania tylko w celu wykazania, że ​​stało się to w przypadku, gdy jesteś zainteresowany . wybrana odpowiedź jest niefortunna, ponieważ nie pozwala na wycinanie krótkich połączeń z połączeniami, takimi jak sen i czekanie.
Nathan Hughes

Odpowiedzi:


173

W IndexProcessorklasie potrzebujesz sposobu ustawienia flagi, która informuje wątek, że będzie musiał zakończyć działanie, podobnie jak zmiennarun , której użyłeś tylko w zakresie klasy.

Kiedy chcesz zatrzymać wątek, ustaw tę flagę, wzywaj join()wątek i poczekaj, aż zakończy się.

Upewnij się, że flaga jest bezpieczna dla wątków, używając zmiennej lotnej lub metod pobierających i ustawiających, które są zsynchronizowane ze zmienną używaną jako flaga.

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

    }
}

Następnie w 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
Zrobiłem dokładnie to samo, co podałeś przykłady w swojej odpowiedzi tuż przed tym, jak wyglądałem, że to zredagowałeś. Świetna odpowiedź! Dziękuję, teraz wszystko działa idealnie :)
Paulius Matulionis

1
Co jeśli logika wątków jest złożona i wywołuje wiele metod innych klas? Nie można wszędzie sprawdzić flagi logicznej. Co wtedy zrobić?
Soteric

Trzeba będzie zmienić projekt kodu, aby zbudować go w taki sposób, że sygnał do Runnable spowoduje wyjście z wątku. Większość zastosowań ma tę pętlę w metodzie uruchamiania, więc zwykle nie występuje problem.
DrYap

3
Co się stanie, jeśli instrukcja join () zgłosi wyjątek InterruptedException?
benzaita,

14
Oddany za rozpowszechnianie złych rad. podejście z ręcznie zwiniętą flagą oznacza, że ​​aplikacja musi zaczekać, aż sen się zakończy, a przerwanie skróci go. Łatwo byłoby to zmodyfikować, aby użyć przerwania # wątku.
Nathan Hughes,

298

Używanie Thread.interrupt()jest całkowicie akceptowalnym sposobem na zrobienie tego. W rzeczywistości jest to prawdopodobnie lepsze niż flaga, jak sugerowano powyżej. Thread.sleepWynika to z faktu, że jeśli prowadzisz przerywaną rozmowę blokującą (np. Lub korzystasz z operacji na kanale java.nio), będziesz w stanie od razu oderwać się od nich.

Jeśli używasz flagi, musisz poczekać na zakończenie operacji blokowania, a następnie możesz sprawdzić swoją flagę. W niektórych przypadkach i tak musisz to zrobić, na przykład używając standardowego InputStream/ OutputStreamktóre nie są przerywane.

W takim przypadku, gdy wątek zostanie przerwany, nie spowoduje to przerwania operacji we / wy, jednak można to łatwo zrobić rutynowo w kodzie (i należy to zrobić w strategicznych punktach, w których można bezpiecznie zatrzymać i wyczyścić)

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

Jak powiedziałem, główną zaletą Thread.interrupt()jest to, że możesz natychmiast zerwać z przerwanych połączeń, czego nie można zrobić przy podejściu flagowym.


32
+1 - Thread.interupt () jest zdecydowanie lepsza niż implementacja tego samego przy użyciu flagi ad-hoc.
Stephen C,

2
Uważam również, że jest to idealny i skuteczny sposób na zrobienie tego. +1
RoboAlex

4
W kodzie jest mała literówka, Thread.currentThread () nie ma nawiasu.
Vlad V

1
W rzeczywistości nie jest preferowane używanie flagi, ponieważ ktoś, kto wejdzie w kontakt z wątkiem, może przerwać go z innego miejsca, powodując jego zatrzymanie i bardzo trudne do debugowania. Zawsze używaj również flagi.
JohnyTex,

W tym konkretnym przypadku wywołanie interrupt()może być w porządku, ale w wielu innych przypadkach tak nie jest (na przykład, jeśli zasób musi zostać zamknięty). Jeśli ktoś zmieni wewnętrzne działanie pętli, musisz pamiętać o przejściu interrupt()na tryb boolowski. Od samego początku wybrałem bezpieczną drogę i użyłem flagi.
m0skit0

25

Prosta odpowiedź: możesz zatrzymać wątek WEWNĘTRZNIE na jeden z dwóch typowych sposobów:

  • Metoda uruchamiania trafia w podprogram powrotu.
  • Metoda Run kończy się i zwraca niejawnie.

Możesz także zatrzymać wątki ZEWNĘTRZNIE:

  • Zadzwoń system.exit(to zabija cały proces)
  • Wywołaj interrupt()metodę obiektu wątku *
  • Sprawdź, czy wątek ma zaimplementowaną metodę, która brzmi tak, jakby działał (jak kill()lub stop())

*: Oczekuje się, że ma to zatrzymać wątek. Jednak to, co faktycznie robi wątek, gdy tak się dzieje, zależy wyłącznie od tego, co napisał programista, gdy utworzyli implementację wątku.

Typowy wzorzec, który widzisz w implementacjach metod uruchamiania, to taki while(boolean){}, gdzie boolean ma zwykle coś o nazwie isRunning, jest zmienną składową swojej klasy wątku, jest zmienny i zazwyczaj dostępny dla innych wątków za pomocą metod ustawiających, np kill() { isRunnable=false; }. Te podprogramy są fajne, ponieważ pozwalają wątkowi zwolnić wszystkie posiadane zasoby przed zakończeniem.


3
„Te podprogramy są fajne, ponieważ pozwalają wątkowi zwolnić wszystkie posiadane zasoby przed zakończeniem”. Nie rozumiem. Możesz doskonale wyczyścić zasoby posiadane przez wątek, używając statusu „oficjalnego” przerwania. Wystarczy to sprawdzić za pomocą Thread.currentThread (). IsInterrupted () lub Thread.interrupt () (w zależności od potrzeb) lub złap InterruptedException i cleanup. Gdzie jest problem
Franz D.

Nie byłem w stanie zrozumieć, dlaczego metoda flagi działa, ponieważ nie zrozumiałem, że zatrzymuje się, gdy powracają uderzenia uruchomienia !!! To było takie proste, drogi panie, dziękuję za zwrócenie na to uwagi, nikt nie zrobił tego wprost.
thahgr

9

Zawsze powinieneś kończyć wątki, sprawdzając flagę w run() pętli (jeśli istnieje).

Twój wątek powinien wyglądać następująco:

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

Następnie możesz zakończyć wątek, dzwoniąc thread.stopExecuting() . W ten sposób nić zostaje zakończona czysto, ale zajmuje to do 15 sekund (z powodu twojego snu). Nadal możesz wywołać thread.interrupt (), jeśli jest to naprawdę pilne - ale preferowanym sposobem powinno być zawsze sprawdzanie flagi.

Aby uniknąć czekania przez 15 sekund, możesz podzielić sen w następujący sposób:

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

            LOGGER.debug("Processing");
        } catch (InterruptedException e) {
        ...

2
to nie jest Thread- implementuje Runnable- nie można wywoływać Threadmetod, chyba że zadeklarujesz to jako, Threadw którym to przypadku nie możesz zadzwonićstopExecuting()
Don Cheadle

7

Zazwyczaj wątek jest przerywany, gdy zostanie przerwany. Dlaczego więc nie użyć natywnej wartości logicznej? Spróbuj 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- Jak mogę zabić wątek? bez użycia stop ();


5

Do synchronizacji wątków wolę używać, CountDownLatchktóry pomaga wątkom czekać na zakończenie procesu. W takim przypadku klasa procesu roboczego jest konfigurowana z CountDownLatchinstancją o podanej liczbie. Wywołanie awaitmetody będzie blokowane, dopóki bieżąca liczba nie osiągnie zera z powodu wywołania countDownmetody lub osiągnięcia limitu czasu. Takie podejście umożliwia natychmiastowe przerwanie wątku bez konieczności oczekiwania na upływ określonego czasu oczekiwania:

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

    }
}

Gdy chcesz zakończyć wykonanie innego wątku, wykonać na odliczanie CountDownLatchi joingwint do głównego wątku:

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

Niektóre dodatkowe informacje. Zarówno flaga, jak i przerwanie są sugerowane w dokumencie 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();
    }
}

W przypadku wątku, który czeka przez długi czas (np. Na dane wejściowe), użyj Thread.interrupt

public void stop() {
     Thread moribund = waiter;
      waiter = null;
      moribund.interrupt();
 }

3
Nigdy nie ignoruj ​​wyjątku InterruptedException. Oznacza to, że jakiś inny kod wyraźnie prosi Twój wątek o zakończenie. Wątek, który ignoruje to żądanie, jest nieuczciwym wątkiem. Prawidłowym sposobem obsługi wyjątku InterruptedException jest wyjście z pętli.
VGR

2

Nie udało mi się przerwać pracy w Androidzie, więc użyłem tej metody, działa idealnie:

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

To może się nie powieść, ponieważ nie powinno to zrobić CheckCheckUpdates volatile. Zobacz docs.oracle.com/javase/specs/jls/se9/html/jls-17.html#jls-17.3 .
VGR

0

Kiedyś spróbuję 1000 razy w mojej funkcji 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();}
        }

    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.