Статьи

InterruptedException и прерывание потоков объяснено

Если бы InterruptedException не был проверенным исключением, вероятно, никто бы этого даже не заметил — что фактически предотвратило бы пару ошибок в течение этих лет. Но поскольку с ним нужно обращаться, многие обращаются с ним неправильно или бездумно. Давайте возьмем простой пример потока, который периодически выполняет некоторую очистку, но большую часть времени находится в спящем режиме.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Cleaner implements Runnable {
 
  Cleaner() {
    final Thread cleanerThread = new Thread(this, "Cleaner");
    cleanerThread.start();
  }
 
  @Override
  public void run() {
    while(true) {
      cleanUp();
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }
 
  private void cleanUp() {
    //...
  }
 
}

Этот код неверен на многих уровнях!

  1. Запуск Thread в конструкторе может быть плохой идеей в некоторых средах, например, некоторые среды, такие как Spring, будут создавать динамический подкласс для поддержки перехвата методов. В итоге мы получим два потока, запущенных из двух экземпляров.
  2. InterruptedException проглатывается, а само исключение не регистрируется должным образом
  3. Этот класс запускает новый поток для каждого экземпляра, вместо этого он должен использовать ScheduledThreadPoolExecutor , совместно используемый многими экземплярами (более надежный и эффективный для памяти)
  4. Также с ScheduledThreadPoolExecutor мы могли бы избежать кодирования спящего / рабочего цикла самостоятельно, а также переключаться на фиксированную скорость, в отличие от представленного здесь поведения с фиксированной задержкой.
  5. И наконец, что не менее важно, нет способа избавиться от этого потока, даже когда на экземпляр Cleaner больше не ссылается что-либо еще.

Все проблемы действительны, но проглатывание InterruptedException является его самым большим грехом. Прежде чем понять причину, давайте немного подумаем, что означает это исключение и как мы можем использовать его для изящного прерывания потоков. Многие блокирующие операции в JDK объявляют об исключении InterruptedException , включая:

  • Object.wait()
  • Thread.sleep()
  • Process.waitFor()
  • AsynchronousChannelGroup.awaitTermination()
  • Различные методы блокировки в java.util.concurrent.* , Например, ExecutorService.awaitTermination() , Future.get() , BlockingQueue.take() , Semaphore.acquire() Condition.await() и многие, многие другие
  • SwingUtilities.invokeAndWait()

Обратите внимание, что блокировка ввода / вывода не вызывает InterruptedException (что является позором). Если все эти классы объявляют InterruptedException , вам может быть интересно, когда это исключение когда-либо выдается?

  • Когда поток заблокирован в каком-либо методе, объявляющем InterruptedException и вы вызываете Thread.interrupt() в таком потоке, наиболее вероятный заблокированный метод немедленно вызовет InterruptedException .
  • Если вы отправили задачу в пул потоков ( ExecutorService.submit() ), и вы вызываете Future.cancel(true) во время выполнения задачи. В этом случае пул потоков будет пытаться прервать поток, выполняющий такую ​​задачу для вас, эффективно прерывая вашу задачу.

Зная, что на самом деле означает InterruptedException , мы хорошо подготовлены для того, чтобы правильно с ним справляться. Если кто-то пытается прервать наш поток, и мы обнаружили его, перехватив InterruptedException , наиболее разумным решением будет позволить этому потоку завершиться, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Cleaner implements Runnable, AutoCloseable {
 
  private final Thread cleanerThread;
 
  Cleaner() {
    cleanerThread = new Thread(this, "Cleaner");
    cleanerThread.start();
  }
 
  @Override
  public void run() {
    try {
      while (true) {
        cleanUp();
        TimeUnit.SECONDS.sleep(1);
      }
    } catch (InterruptedException ignored) {
      log.debug("Interrupted, closing");
    }
  }
 
  //...  
 
  @Override
  public void close() {
    cleanerThread.interrupt();
  }
}

Обратите внимание, что блок try-catch теперь окружает цикл while. Таким образом, если sleep() генерирует InterruptedException , мы вырвемся из цикла. Вы можете утверждать, что мы должны регистрировать трассировку стека InterruptedException . Это зависит от ситуации, так как в этом случае прерывание потока — это то, что мы действительно ожидаем, а не сбой. Но это зависит от вас. Суть в том, что если sleep() прерывается другим потоком, мы быстро убегаем от run() целом. Если вы очень осторожны, вы можете спросить, что произойдет, если мы cleanUp() поток, пока он находится в cleanUp() а не в спящем режиме? Часто вы сталкиваетесь с ручным флагом как это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private volatile boolean stop = false;
 
@Override
public void run() {
  while (!stop) {
    cleanUp();
    TimeUnit.SECONDS.sleep(1);
  }
}
 
@Override
public void close() {
  stop = true;
}

Однако обратите внимание, что флаг stop (он должен быть volatile !) Не будет прерывать операции блокировки, нам нужно дождаться завершения sleep() . С другой стороны, можно утверждать, что явный flag дает нам лучший контроль, поскольку мы можем отслеживать его значение в любое время. Оказывается, прерывание потока работает так же. Если кто-то прервал поток, когда он выполнял неблокирующие вычисления (например, внутри cleanUp() ), такие вычисления не прерываются немедленно. Однако поток помечается как прерванный, и каждая последующая блокирующая операция (например, sleep() ) просто немедленно генерирует InterruptedException — поэтому мы не потеряем этот сигнал.

Мы также можем воспользоваться этим фактом, если напишем неблокирующий поток, который все еще хочет воспользоваться возможностью прерывания потока. Вместо того, чтобы полагаться на InterruptedException мы просто должны периодически проверять Thread.isInterrupted() :

1
2
3
4
5
public void run() {
  while (Thread.currentThread().isInterrupted()) {
    someHeavyComputations();
  }
}

Выше, если кто-то прерывает наш поток, мы откажемся от вычислений, как только someHeavyComputations() . Если он будет длиться два долго или бесконечно, мы никогда не обнаружим флаг прерывания. Интересно, что interrupted флаг не является одноразовым блокнотом . Мы можем вызвать Thread.interrupted() вместо isInterrupted() , что сбросит флаг interrupted и мы можем продолжить. Иногда вы можете игнорировать прерванный флаг и продолжать работать. В этом случае interrupted() может пригодиться. Кстати, я (неточно) называю «добытчики», которые меняют состояние наблюдаемого объекта, « Гейзенгеттеры ».

Обратите внимание на Thread.stop()

Если вы программист старой школы, вы можете вызвать Thread.stop() , который более 10 лет считается устаревшим . В Java 8 планировалось « отменить ее реализацию» , но в 1.8u5 она все еще есть. Тем не менее, не используйте его и не выполняйте рефакторинг кода с помощью Thread.stop() в Thread.interrupt() .

Uninterruptibles питания из гуавы

Редко вы можете вообще игнорировать InterruptedException . В этом случае взгляните на Uninterruptibles питания из Гуавы. Он имеет множество служебных методов, таких как sleepUninterruptibly() или awaitUninterruptibly(CountDownLatch) . Просто будь осторожен с ними. Я знаю, что они не объявляют InterruptedException (что может быть немного), но они также полностью предотвращают прерывание текущего потока — что довольно необычно.

Резюме

Теперь я надеюсь, что вы уже поняли, почему определенные методы генерируют InterruptedException . Основные выводы:

  • Пойманный InterruptedException должен быть обработан должным образом — в большинстве случаев это означает разрыв текущей задачи / цикла / потока полностью
  • Глотание InterruptedException редко является хорошей идеей
  • Если поток был прерван, пока он не находился в блокирующем вызове, используйте isInterrupted() . Также ввод метода блокировки, когда поток уже был прерван, должен немедленно вызвать InterruptedException .