Теория и практика Java: Работа с InterruptedException

Вы его перехватили, что же теперь с ним делать?

Многие методы языка Java™, такие как Thread.sleep() и Object.wait(), выдают исключение InterruptedException. На него нельзя не обратить внимания, потому что это отмеченное исключение, но что же с ним делать? В этом месяце в очередной статье из серии Теория и практика Java эксперт по параллельности Брайан Гетц объясняет, что означает InterruptedException, почему оно генерируется, и что с ним делать.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



27.02.2007

Эта история, наверное, вам знакома: вы пишете тестовую программу, и вам нужна на некоторое время пауза, поэтому вы вызываете Thread.sleep(). Но тут компилятор или IDE отклоняет это, заявляя, что вы не отреагировали на отмеченное InterruptedException. Что такое InterruptedException, и почему вам приходится с ним иметь дело?

Наиболее типичной реакцией на InterruptedException является его игнорирование, когда вы перехватываете его и ничего с ним не делаете (или, возможно, регистрируете его, что вряд ли намного лучше) - как мы увидим позднее в Листинге 4. К сожалению, этот подход отбрасывает важную информацию о том, что произошло прерывание, что может подвергнуть риску способность приложения отменять функции или своевременно прекратить работу.

Методы блокирования

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

Метод блокирования отличается от обычного метода, которому требуется много времени для запуска. Завершение обычного метода зависит только от того, как много работы вы на него возложили, и имеются ли в наличии соответствующие компьютерные ресурсы (циклы центрального процессора и память). Завершение метода блокирования, с другой стороны, также зависит от некоего внешнего события, такого как прекращение действия таймера, завершение ввода-вывода, или действие другого потока (снимающего блокировочный замок, устанавливающего метку или ставящего в очередь задачу). Обычные методы завершаются, как только их работа выполнена, но методы блокирования менее предсказуемы, поскольку они зависят от внешних событий. Методы блокирования снижают время реакции, поскольку бывает трудно предсказать, когда они завершатся.

Поскольку методы блокирования потенциально могут занять сколь угодно много времени, если ожидаемое ими событие никогда не произойдет, то зачастую очень полезно для операций блокирования быть отменяемыми (cancelable). (Зачастую для долго работающих неблокирующих методов также полезно быть отменяемыми.) Отменяемой операцией является та, которая может быть извне доведена до завершения прежде, чем она самостоятельно завершится обычным путем. Механизм прерывания, обеспечиваемый Thread и поддерживаемый Thread.sleep() и Object.wait(), является механизмом отмены; он позволяет одному потоку запрашивать, чтобы другой поток прекратил выполнение того, что он делал ранее. Когда метод выдает InterruptedException, то он говорит вам, что если поток, выполняющий метод, прерван, то он предпримет попытку прекратить то, что он делает, вернуться на ранний этап, и указать его досрочный возврат путем выдачи InterruptedException. Рабочие методы библиотек блокирования должны реагировать на прерывание и выдавать InterruptedException, так, чтобы их можно было использовать в отменяемых функциях без риска для ответной реакции.

Прерывание потока

Каждый поток имеет связанное с ним булево свойство, которое отображает его статус прерывания. Статус прерывания изначально имеет значение false; когда поток прерывается каким-либо другим потоком путем вызова Thread.interrupt(), то происходит одно из двух. Если другой поток выполняет прерываемый метод блокирования низкого уровня, такой как Thread.sleep(), Thread.join() или Object.wait(), то он разблокируется и выдает InterruptedException. Иначе, interrupt() просто устанавливает статус прерывания потока. Код, действующий в прерванном потоке, может позднее обратиться к статусу прерывания, чтобы посмотреть, был ли запрос на прекращение выполняемого действия; статус прерывания может быть прочитан с помощью Thread.isInterrupted(), и может быть прочитан и сброшен за одну операцию при помощи неудачно названного Thread.interrupted().

Прерывание является коллективным механизмом взаимодействия (cooperative mechanism). Когда один поток прерывает другой, прерванный поток не обязательно немедленно прекратит делать то, что он делал. Напротив, прерывание является вежливым способом, чтобы попросить другой поток прекратить то, что он делает, если он не против, когда ему это будет удобно. Некоторые методы, например, Thread.sleep(), серьезно относятся к такой просьбе, но методы не обязаны обращать внимание на прерывание. Неблокирующие методы, выполнение которых занимает много времени, могут положительно отнестись к просьбам о прерывании, путем опроса статуса прерывания, и в случае прерывания вернуться к более раннему этапу. Вы можете проигнорировать запрос о прерывании, но это может нанести ущерб ответной реакции.

Одним из преимуществ коллективной природы прерывания является то, что оно обеспечивает большую гибкость для безопасного построения отменяемых функций. Редко требуется немедленно остановить какую-либо функцию; структуры программных данных могут находиться в противоречивом состоянии, если функции были отменены во время обновления. Прерывание позволяет отменяемой функции завершить любую производящуюся работу, восстановить инварианты, уведомить другие функции об отмене, а затем прекратить работу.


Работа с InterruptedException

Если выдача InterruptedException означает, что метод является блокирующим, то вызов метода блокирования означает, что ваш метод также является блокирующим, и у вас должна быть стратегия для работы с InterruptedException. Зачастую наиболее простой стратегией является генерирование собственного InterruptedException, как показано в методах putTask() и getTask() в Листинге 1. Выполняя это, вы также делаете ваш метод восприимчивым к прерыванию, и это для этого требуется всего-навсего добавить InterruptedException в вашу конструкцию throws.

Листинг 1. Распространение InterruptedException на вызывающие операторы без его перехвата
public class TaskQueue {
    private static final int MAX_TASKS = 1000;

    private BlockingQueue<Task> queue 
        = new LinkedBlockingQueue<Task>(MAX_TASKS);

    public void putTask(Task r) throws InterruptedException { 
        queue.put(r);
    }

    public Task getTask() throws InterruptedException { 
        return queue.take();
    }
}

Иногда необходимо произвести некоторую очистку, прежде чем распространить исключение. В этом случае вы можете перехватить InterruptedException, выполнить очистку, а затем повторно сгенерировать исключение. В Листинге 2, механизме подбора игроков в онлайновом игровом портале, показана именно такая технология. Метод matchPlayers() ожидает прибытия двух игроков, а затем начинает новую игру. Если она прервана после того, как появился один игрок, но до появления второго, то он ставит игрока в конец очереди, прежде чем повторно выдать InterruptedException, так, чтобы запрос игрока не был утерян.

Листинг 2. Выполнение специализированной (task-specific) очистки перед повторной выдачей InterruptedException
public class PlayerMatcher {
    private PlayerSource players;

    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }

    public void matchPlayers() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}

Не поглощайте прерывания

Иногда генерирование InterruptedException не является опцией, как в случае задачи, определяемой Runnable, которая вызывает прерываемый метод. В этом случае вы не можете повторно выдать InterruptedException, но вообще ничего не делать нельзя. Когда метод блокирования обнаруживает прерывание и выдает InterruptedException, то он очищает статус прерывания. Если вы перехватили InterruptedException, но не можете повторно его сгенерировать, то вы должны сохранить подтверждение того, что прерывание произошло так, чтобы вышестоящий в стеке вызовов код мог узнать о прерывании и среагировать на него, если он хочет это сделать. Эта задача выполняется с помощью вызова interrupt(), чтобы "повторно прервать" текущий поток, как показано в Листинге 3. По крайней мере, когда бы вы ни перехватили InterruptedException и не выдали его повторно, еще раз прервите текущий поток перед возвратом.

Листинг 3. Восстановление статуса прерывания после перехвата InterruptedException
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;

    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }

    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException e) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}

Худшее, что вы можете сделать с InterruptedException - это его поглощение - перехват без повторной его выдачи и без переподтверждения статуса прерывания потока. Стандартный подход к работе с незапланированной ошибкой - перехватить ее и зарегистрировать - также расценивается как поглощение (swallowing) прерывания, поскольку вышестоящий в стеке вызовов код не сможет узнать об этом. (Регистрирование InterruptedException также просто бессмысленно, потому что к тому времени, когда человек прочитает журнал, будет слишком поздно что-либо предпринимать.) В Листинге 4 показан слишком типичный шаблон поглощения прерывания:

Листинг 4. Поглощение прерывания - не делайте этого
// Don't do this 
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;

    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }

    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException swallowed) { 
             /* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
         }
    }
}

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


Реализация отменяемых задач

В спецификации языка не дается какой-либо конкретной семантики прерывания, но в больших программах сложно получить какую-нибудь иную семантику прерывания, кроме отмены. В зависимости от функции, пользователь может запросить отмену через GUI или через сетевой механизм, например JMX или Web-сервисы. Она также может запрашиваться логикой программы. Например, Web crawler (поисковый агент) может автоматически прекратить свою работу в случае обнаружения переполнения диска, или параллельный алгоритм может запустить множество потоков для поиска в различных областях пространства решения и отменить их, как только один из них его найдет.

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

Единственный раз, когда допустимо поглотить прерывание, это когда вы знаете, что поток должен вот-вот завершить работу. Этот сценарий имеет место только когда класс, вызывающий прерываемый метод, является частью Thread, а не Runnable или универсального библиотечного кода, как показано в Листинге 5. Он создает поток, который перечисляет простые числа до тех пор, пока он не будет прерван, и позволяет потоку завершить работу после прерывания. Ищущий простые числа цикл проверяет наличие прерывания в двух местах: один раз, опрашивая метод isInterrupted() в заголовке цикла с проверкой условия, и еще раз - при вызове метода блокирования BlockingQueue.put().

Листинг 5. Сообщения interrupt можно поглощать, если вы знаете, что поток скоро завершит работу
public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;

    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted())
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            /* Allow thread to exit */
        }
    }

    public void cancel() { interrupt(); }
}

Непрерываемое блокирование

Не все методы блокирования выдают InterruptedException. Классы входных и выходных потоков могут блокировать ожидание ввода-вывода для завершения, но они не выдают InterruptedException, а также не возвращаются на ранний этап, если их прервать. Тем не менее, в случае сокета I/O, если поток закрывает сокет, блокирование операции ввода-вывода данного сокета в других потоках завершится раньше с исключением SocketException. Неблокирующие I/O-классы в java.nio также не поддерживают прерываемый ввод-вывод, но операции блокирования можно отменить сходным образом, закрыв канала или запросив активацию в Selector. Аналогично, попытка получить внутреннюю блокировку (вводом блока synchronized) не может быть прервана, но ReentrantLock поддерживает прерываемый режим захвата.

Неотменяемые задачи

Некоторые задачи просто отказываются быть прерванными, что делает их неотменяемыми. Однако даже неотменяемые задачи должны попытаться сохранить статус прерывания на случай, если вышестоящий в стеке вызовов код захочет воздействовать на прерывание после завершения неотменяемой задачи. В Листинге 6 показан метод, ожидающий в очереди блокировок до тех пор, пока элемент не станет доступным, не обращая внимания на то, был ли он прерван. Как сознательный гражданин, он восстанавливает статус прерывания в финальном блоке после его завершения, так чтобы не лишить вызывающие операторы запроса прерывания. (Он не мог восстановить статус прерывания ранее, поскольку это привело бы к бесконечному циклу - BlockingQueue.take() могло бы опросить статус прерывания немедленно при входе и выдать InterruptedException, если он обнаружит установку статуса прерывания.)

Листинг 6. Неотменяемая задача, восстанавливающая статус прерывания перед возвратом
public Task getNextTask(BlockingQueue<Task> queue) {
    boolean interrupted = false;
    try {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException e) {
                interrupted = true;
                // fall through and retry
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

Резюме

Вы можете использовать коллективный механизм прерывания, обеспечиваемый платформой Java для конструирования гибких алгоритмов отмены. Функции (activities) могут решать, являются ли они отменяемыми или нет, насколько им следует быть восприимчивыми к прерыванию, и они могут задержать прерывание для выполнения специализированной очистки, если немедленный возврат подвергнет риску целостность приложения. Даже если вы захотите полностью проигнорировать прерывание в вашем коде, убедитесь в восстановлении статуса прерывания, если вы перехватили InterruptedException, и не генерируйте его повторно, чтобы вызывающий его код не был лишен информации о том, что произошло прерывание.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=198196
ArticleTitle=Теория и практика Java: Работа с InterruptedException
publish-date=02272007