вторник, 23 мая 2017 г.

Что вы делаете с InterruptedException?

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


Начнем с этого кода:

while (true) {
  // Nothing
}
Что оно делает? Ничего, просто вращает бесконечно процессор. Можем ли мы его прекратить? Не на Java. Он остановится, когда вся JVM остановится, когда вы нажмете Ctrl-C. В Java нет способа завершить поток, если поток не завершится сам по себе. Это тот принцип, который мы должны иметь в виду, и все остальное будет просто очевидно.

Давайте поместим этот бесконечный цикл в поток:
Thread loop = new Thread(
  new Runnable() {
    @Override
    public void run() {
      while (true) {
      }
    }
  }
);
loop.start();
// Now how do we stop it?
Итак, как остановить поток, когда нам нужно его остановить?

Вот как это разработано на Java. В каждом потоке есть флаг, который мы можем установить снаружи. И поток может иногда проверять его и останавливать его выполнение. Добровольно! Вот как это сделать:
Thread loop = new Thread(
  new Runnable() {
    @Override
    public void run() {
      while (true) {
        if (Thread.interrupted()) {
          break;
        }
        // Continue to do nothing
      }
    }
  }
);
loop.start();
loop.interrupt();
Это единственный способ попросить поток остановиться. В этом примере используются два метода. Когда я вызываю loop.interrupt (), флаг устанавливается в true где-то внутри цикла потока. Когда я вызываю interrupted (), флаг возвращается и сразу устанавливается в false. Да, это дизайн метода. Он проверяет флаг, возвращает его и устанавливает значение false. Это ужасно, я знаю.

Таким образом, если я никогда не вызову Thread.interrupted () внутри потока и не выйду, когда флаг верен, никто не сможет остановить меня. Буквально, я просто проигнорирую их вызовы interrupt (). Они попросят меня остановиться, но я проигнорирую их. Они не смогут меня перебить.

Таким образом, чтобы суммировать то, что мы узнали до сих пор, правильно спроектированный поток будет проверять этот флаг время от времени и останавливаться грациозно. Если код не проверяет флаг и никогда не вызывает Thread.interrupted (), он принимает тот факт, что рано или поздно он будет прерван нажатием Ctrl-C.

Код должен быть либо пуленепробиваем, либо готов к перерыву, ничего между этим.

Звучит логично? Я надеюсь, что это так.

Теперь, в JDK есть несколько методов, которые проверяют флаг для нас и бросают InterruptedException, если он установлен. Например, так устроен метод Thread.sleep () (с очень примитивным подходом):
public static void sleep(long millis)
  throws InterruptedException {
  while (/* You still need to wait */) {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    // Keep waiting
  }
}
Почему это делается именно так? Почему он не может просто ждать и никогда не проверять флаг? Ну, я считаю, что это сделано по уважительной причине. И причина в следующем (поправьте меня, если я ошибаюсь): код должен быть либо готовым к пулям, либо готовым к прерыванию, ничего между этим.

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

Именно поэтому InterruptedException является checked исключением. Его дизайн говорит вам, что если вы хотите приостановить работу на несколько миллисекунд, подготовьте свой код для прерывания. Вот как это выглядит на практике:
try {
  Thread.sleep(100);
} catch (InterruptedException ex) {
  // Stop immediately and go home
}
Ну, вы могли бы позволить ему подняться на более высокий уровень, где будут ответственность за обработку. Дело в том, что кто-то должен поймать его и что-то сделать с потоком. В идеальном случае просто остановите его, так как это и есть флаг. Если InterruptedException бросается, это означает, что кто-то проверил флаг, и наш поток должен завершить то, что он делает как можно скорее.

Владелец потока не хочет больше ждать. И мы должны уважать решение нашего владельца.

Таким образом, поймав InterruptedException, вы должны делать все, что нужно, чтобы завершить то, что вы делаете, и выйти.

Теперь посмотрим снова на код Thread.sleep ():
public static void sleep(long millis)
  throws InterruptedException {
  while (/* ... */) {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
  }
}
Помните, Thread.interrupted () не только возвращает флаг, но также устанавливает его значение false. Таким образом, после того, как InterruptedException выбрасывается, флаг сбрасывается. Поток больше не знает ничего о запросе прерывания, отправленном владельцем.

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

Видите, к чему я клоню? Очень важно не потерять это InterruptedException. Мы не можем просто проглотить его и двигаться дальше. Это было бы серьезным нарушением всей идеи многопоточности Java. Наш владелец (владелец нашего потока) просит нас остановиться, и мы просто игнорируем его. Это плохая идея.

Это то, что большинство из нас делает с InterruptedException:
try {
  Thread.sleep(100);
} catch (InterruptedException ex) {
  throw new RuntimeException(ex);
}
Это выглядит логично, но это не гарантирует, что более высокий уровень фактически остановит все и выйдет. Они могут просто поймать исключение времени исполнения, и поток останется в живых. Владелец темы будет разочарован.

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

Это то, что мы должны сделать:
try {
  Thread.sleep(100);
} catch (InterruptedException ex) {
  Thread.currentThread().interrupt(); // Here!
  throw new RuntimeException(ex);
}
Мы устанавливаем флаг обратно в true!

Теперь никто не будет обвинять нас в безответственном отношении к ценному флагу. Мы нашли его в истинном статусе, очистили его, вернули ему true и выбросили исключение во время выполнения. Что происходит дальше, нам все равно.



Комментариев нет:

Отправить комментарий