Interrupted Exception

04 September 2009 java concurrency

Недавно еще раз встретился с некорректной обработкой InterruptedException в java. InterruptedException — это checked exception генерируемый многими методами стандартной библиотеки, которые блокируют поток исполнения. К таким относятся: interruptible версии lock’ов, метод Thread.sleep(), некоторые операции над блокирующими очередями, некоторые операции над каналами и другие.

По своей сути, InterruptedException сигнализирует о том, что поток просят завершить его работу. При этом вас не просят немедленно завершить свою работу. Вас просят корректно завершить работу. На это может понадобится некоторое время.

Прерывание потока осуществляется при помощи метода Thread.interrupt(). Существует два способа которыми JVM уведомляет поток о том, что его прерывают. Первый — это собственно InterruptedException. Второй — это флаг потока INTERRUPT, который может быть получен при помощи метода Thread.isInterrupted(). Игнорирование второго метода сигнализирования о прерывании и является типичной ошибкой.

Важно понимать, что interrupt адресуется не только вам (коду, который выполняется в потоке), но и владельцу потока. Другими словами, не только вы в этом потоке заинтересованы в том, чтобы определить что наступило прерывание.

try {
  Object o = queue.take();
} catch ( InterruptedException e ) {}

Данный код неверен, потому что он “давит” сигнал прерывания. Если этот код выполняется в thread pool’е, то на этом таске worker (который вызвал вас) должен был бы завершить исполнение задач, так как поток получил прерывание. Но этого не произойдет потому что сигнал прерывания был перехвачен вышеприведенным кодом. Как правило, это выражается в том, что после посылки kill сигнала java машине у нее остаются висеть потоки worker’ов из-за чего JVM не может завершить свою работу. В данном случае единственный выход — kill -9.

Следующий код так же некорректен, потому что мы “маскируем” interrupt в исключительную ситуацию другого типа. Тем самым он не дает возможности вызывающей стороне зафиксировать ситуацию прерывания.

try { 
  Object o = queue.take();
} catch ( InterruptedException e ) {
  throw new RuntimeException(e);
}

Корректный код будет выглядеть следующим образом.

try {
  Object o = queue.take();
} catch ( InterruptedException e ) {
  Thread.currentThread().interrupt();
}

Здесь мы ловим InterruptedException и выставляем флаг потока сигнализирующий о прерывании.

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

Внимательный читатель мог задать себе вопрос: “А зачем два способа сигнализации? Разве нельзя обойтись одним?”.

Зачем нужен Thread.isInterrupted()?

InterruptedException бывает неудобен по причине того, что это checked exception. Это значит что компилятор заставляет вас или обработать его по месту, или указать в своем контракте. В случае, если вы не можете указать InterruptedException в своем контракте (например, вы имплементируете интерфейс где в контракте InterruptedException не указан), флаг потока остается единственным способом передать вызывающей стороне информацию о прерывании.

Зачем нужен InterruptedException?

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