Статьи

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

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

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, самое разумное, что нужно сделать, — позволить этому потоку завершиться, например

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() методе, а не в спящем режиме? Часто вы сталкиваетесь с ручным флагом как это:

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() периодически проверять
:

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