Если бы 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() {
//...
}
}
Этот код неверен на многих уровнях!
- Запуск
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
, самое разумное, что нужно сделать, — позволить этому потоку завершиться, например
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