Если бы 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() { //... } } |
Этот код неверен на многих уровнях!
- Запуск
Thread
в конструкторе может быть плохой идеей в некоторых средах, например, некоторые среды, такие как Spring, будут создавать динамический подкласс для поддержки перехвата методов. В итоге мы получим два потока, запущенных из двух экземпляров. -
InterruptedException
проглатывается, а само исключение не регистрируется должным образом - Этот класс запускает новый поток для каждого экземпляра, вместо этого он должен использовать
ScheduledThreadPoolExecutor
, совместно используемый многими экземплярами (более надежный и эффективный для памяти) - Также с
ScheduledThreadPoolExecutor
мы могли бы избежать кодирования спящего / рабочего цикла самостоятельно, а также переключаться на фиксированную скорость, в отличие от представленного здесь поведения с фиксированной задержкой. - И наконец, что не менее важно, нет способа избавиться от этого потока, даже когда на экземпляр
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
.
Ссылка: | О прерывании и прерывании потоков объяснил наш партнер по JCG Томаш Нуркевич из блога Java и соседей . |