Главная страница
Навигация по странице:

  • 7.2 Остановка служб основанных на потоках

  • 7.2.1 Пример: Сервис логирования

  • Листинг 7.15

  • Листинг 7.17

  • 7.2.4 Пример: одноразовая служба выполнения

  • Листинг 7.20

  • 7.3 Обработка аварийного завершения потока

  • Листинг 7.23

  • 7.4 Завершение работы JVM

  • При участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница15 из 34
    1   ...   11   12   13   14   15   16   17   18   ...   34
    Листинг 7.12 Инкапсуляция нестандартной отмены в задачу с помощью метода newTaskFor
    Класс
    SocketUsingTask реализует интерфейс
    CancellableTask и определяет метод
    Future.cancel
    , используемый для закрытия сокета, а также вызова метода super.cancel
    . Если класс
    SocketUsingTask отменяется через собственный экземпляр
    Future
    , сокет закрывается и выполнение потока прерывается. Это
    повышает отзывчивость задачи к запросам на отмену: она может не только безопасно вызывать прерываемые блокирующие методы, оставаясь отзывчивой к запросам на отмену, но также может вызывать блокирующие методы ввода/вывода сокета.
    7.2 Остановка служб основанных на потоках
    Приложения обычно создают службы, владеющие потоками, например пулы потоков, и время жизни этих служб обычно превышает время существования создавших их методов. Если приложение должно завершиться корректно, потоки, принадлежащие этим службам, также должны быть завершены. Поскольку нет упреждающего способа остановить поток, их необходимо убедить самостоятельно завершить свою работу.
    Здравомыслящие практики инкапсуляции диктуют, что не следует манипулировать потоком - прерывать его, изменять его приоритет и т. д. - если только вы не являетесь его владельцем. Формально API потока не имеет концепции
    “владения потоком”: поток представлен объектом
    Thread
    , который может свободно совместно использоваться, как и любой другой объект. Однако, имеет смысл думать о потоке как об имеющем владельца, и обычно таковым оказывается класс, создавший поток. Таким образом, пул потоков владеет своими рабочими потоками, и если эти потоки должны быть прерваны, пул потоков должен позаботиться об этом.
    Как и в случае с любым другим инкапсулированным объектом, владение потоком не является транзитивным: приложение может владеть службой, а служба может владеть рабочими потоками, но приложение не владеет рабочими потоками и поэтому не должно пытаться остановить их напрямую. Вместо этого служба должна предоставлять методы жизненного цикла для завершения работы, которые также завершают работу собственных потоков; затем приложение может завершить работу службы, а служба может завершить работу потоков. Интерфейс
    ExecutorService предоставляет методы shutdown и методы shutdownNow
    ; другие службы, владеющие потоками, должны обеспечивать аналогичный механизм завершения работы.
    Предоставляйте методы жизненного цикла всякий раз, когда срок жизни службы, владеющей потоком, превышает срок жизни метода, её создавшего.
    7.2.1 Пример: Сервис логирования
    Большинство серверных приложений использует логирование, которое может быть реализовано так же просто, как вставка в код операторов println. Поточные классы, такие как
    PrintWriter
    , потокобезопасны, так что этот простой подход не требует выполнения явной синхронизации
    84
    . Однако, как мы увидим в разделе 11.6, встроенный в код механизм ведения журнала может приводить к заметным затратам производительности в приложениях большого размера. Другой
    84
    Если вы логируете несколько строк как части одного сообщения журнала, может потребоваться дополнительная блокировка на стороне клиента для предотвращения нежелательного чередования вывода из нескольких потоков. Если два потока логируют многострочные трассировки стека (
    stack
    traces
    )
    в один и тот же поток с вызовом метода println на каждую строку, результаты будут непредсказуемо чередоваться и вполне могут сойти за одну большую, но бессмысленную трассировку стека.
    альтернативой при вызове метода log,
    является помещение сообщения в очередь для обработки другим потоком.
    Класс
    LogWriter из листинга 7.13 демонстрирует пример простой службы логирования, в которой операция логирования перемещена в отдельный логирующий поток. Вместо того чтобы иметь поток, который производит сообщения и тут же их записывает непосредственно в выходной поток, класс
    LogWriter передает сообщения логирующему потоку с помощью класса
    BlockingQueue
    , и логирующий поток записывает их. Этот дизайн подразумевает несколько производителей и одного потребителя: любая активность, вызывающая метод log выступает в качестве производителя, а фоновый логирующий поток выступает в качестве потребителя. Если логирующий поток отстаёт, класс
    BlockingQueue
    , в конечном счёте, заблокирует производителей, пока логирующий поток их не нагонит. public class LogWriter { private final BlockingQueue queue; private final LoggerThread logger; public LogWriter(Writer writer) { this.queue = new LinkedBlockingQueue(CAPACITY); this.logger = new LoggerThread(writer);
    } public void start() { logger.start(); } public void log(String msg) throws InterruptedException { queue.put(msg);
    } private class LoggerThread extends Thread { private final PrintWriter writer; public void run() { try { while (true) writer.println(queue.take());
    } catch(InterruptedException ignored) {
    } finally { writer.close();
    }
    }
    }
    }
    Листинг 7.13 Служба логирования производитель-потребитель без поддержки завершения работы
    Чтобы служба подобная классу
    LogWriter была полезна в продуктиве, нам нужен способ завершения работы логирующего потока, такой, чтобы он не препятствовал нормальному завершению работы JVM. Остановить логирующий поток достаточно просто, поскольку он многократно вызывает чувствительный к прерыванию метод take
    ; если логирующий поток будет изменён и будет завершать
    свою работу при перехвате исключения
    InterruptedException
    , то прерывание логирующего потока остановит работу службы.
    Однако простой выход из логирующего потока является не очень удовлетворительным механизмом завершения работы. Такое резкое завершение работы приведёт к сбросу логируемых сообщений, ожидающих записи в лог, но, что ещё более важно, потоки, заблокированные в методе log
    , никогда не будут
    разблокированы, так как очередь заполнена. Отмена выполнения активности производителя-потребителя требует отмены и производителей, и потребителей.
    Прерывание логирующего потока имеет дело с потребителем, но поскольку производители в данном случае не являются отдельными потоками, их отменить значительно сложнее.
    Другой подход к завершению работы класса
    LogWriter заключается в том, чтобы установить значение флага "запрос на завершение работы", чтобы предотвратить отправку дальнейших сообщений, как показано в листинге 7.14.
    Получив уведомление о запросе на завершение работы, потребитель мог бы тогда
    “осушить” очередь, записывая любые ожидающие сообщения в лог и разблокировав всех производителей, заблокированных в методе log
    . Однако такой подход содержит в себе предпосылки для возникновения условия гонок, что делает его ненадежным. Реализация метода log представляет собой последовательность операций проверить-затем-выполнить: производители могли бы наблюдать, что работа службы ещё не была завершена, и продолжать класть сообщения в очередь, даже после завершения работы потребителя, таким образом, вновь возникает риск того, что производители могли бы заблокироваться в методе log и никогда не разблокироваться. Есть приёмы, которые уменьшают вероятность возникновения такой ситуации (например, потребитель ждёт несколько секунд, прежде чем объявить очередь опустошённой), но они не меняют механизмов, лежащих в основе проблемы, просто уменьшают вероятность того, что по этой причине произойдёт сбой. public void log(String msg) throws InterruptedException { if (!shutdownRequested) queue.put(msg); else throw new IllegalStateException("logger is shut down");
    }
    Листинг 7.14 Ненадежный способ добавить поддержку завершения работы в службу логирования
    Способ обеспечения надежного завершения работы класса
    LogWriter заключается в том, чтобы исправить условия, при которых возникает состояние гонки, что означает, что отправка новых сообщений в журнал должна происходить атомарно. Но мы не хотим удерживать блокировку при попытке поставить сообщение в очередь, так как метод put может быть заблокирован. Вместо этого мы можем атомарно проверить, была ли работа службы завершена и по условию, если служба работает, увеличить значение счетчика, чтобы “зарезервировать” право на отправку сообщения, как показано в классе
    LogService в листинге 7.15. public class LogService { private final BlockingQueue queue; private final LoggerThread loggerThread;
    private final PrintWriter writer;
    @GuardedBy("this") private boolean isShutdown;
    @GuardedBy("this") private int reservations; public void start() { loggerThread.start(); } public void stop() { synchronized (this) { isShutdown = true; } loggerThread.interrupt();
    } public void log(String msg) throws InterruptedException { synchronized (this) { if (isShutdown) throw new IllegalStateException(...);
    ++reservations;
    } queue.put(msg);
    } private class LoggerThread extends Thread { public void run() { try { while (true) { try { synchronized (LogService.this) { if (isShutdown && reservations == 0) break;
    }
    String msg = queue.take(); synchronized (LogService.this) {
    --reservations;
    } writer.println(msg);
    } catch (InterruptedException e) { /
    *
    retry
    *
    / }
    }
    } finally { writer.close();
    }
    }
    }
    }
    Листинг 7.15 Добавление надёжного механизма отмены классу
    LogWriter
    7.2.2 Завершение работы ExecutorService
    Ранее, в разделе 6.2.4, мы уже видели, что класс ExecutorService предлагает два способа завершения работы: плавное завершение работы с помощью метода shutdown и резкое завершение работы с помощью метода shutdownNow
    . При резком завершении работы, метод shutdownNow, после попытки отменить все
    активно выполняемые задачи, возвращает список задач, которые еще не были запущены.
    Два различных варианта завершения предлагают компромисс между безопасностью и отзывчивостью: резкое завершение быстрее, но более рискованно, потому что задачи могут быть прерваны в процессе выполнения, в свою очередь нормальное завершение медленнее, но безопаснее, потому что класс
    ExecutorService не завершит свою работу до тех пор, пока все поставленные в очередь задачи не будут обработаны. Другим службам-владельцам потоков следует рассмотреть возможность предоставления аналогичного набора режимов завершения работы.
    Простые программы могут легко запускать и завершать работу глобального экземпляра службы
    ExecutorService из метода main
    . Более сложные программы, вероятнее всего, инкапсулируют экземпляр
    ExecutorService в службу более высокого уровня, которая предоставляет собственные методы управления жизненным циклом, например такую, как вариант реализации класса
    LogService в листинге 7.16, который делегирует управление потоками экземпляру
    ExecutorService
    , вместо самостоятельного управления ими. Инкапсуляция экземпляра
    ExecutorService расширяет цепочку владения от приложения к потоку службы, добавив ещё одну ссылку; каждый член цепочки управляет жизненным циклом служб или потоков, которыми он владеет. public class LogService { private final ExecutorService exec = newSingleThreadExecutor(); public void start() { } public void stop() throws InterruptedException { try {
    exec.shutdown();
    exec.awaitTermination(TIMEOUT, UNIT);
    } finally { writer.close();
    }
    } public void log(String msg) { try { exec.execute(new WriteTask(msg));
    } catch (RejectedExecutionException ignored) { }
    }
    }
    Листинг 7.16 Служба логирования использующая интерфейс
    ExecutorService
    7.2.3 Ядовитые пилюли
    Еще один способ убедить службу типа производитель-потребитель завершить свою работу, это использовать ядовитую пилюлю (poison pill): узнаваемый объект, помещенный в очередь, что трактуется как “остановитесь, когда вы это получите”.
    В случае очереди FIFO ядовитые пилюли гарантируют, что потребители закончат работу со своей очередью перед закрытием, так как любая работа, представленная
    до отправки ядовитой пилюли, будет извлечена перед пилюлей; производители не должны отправлять какую-либо работу после помещения ядовитой таблетки в очередь. Класс
    IndexingService
    , представленный в листингах 7.17, 7.18 и 7.19, демонстрирует версию примера поиска на рабочем столе из листинга 5.8, реализованную с помощью одного производителя и одного потребителя, в которой для выключения службы используется ядовитая пилюля. public class IndexingService {
    private static final File POISON = new File("");
    private final IndexerThread consumer = new IndexerThread(); private final CrawlerThread producer = new CrawlerThread(); private final BlockingQueue queue; private final FileFilter fileFilter; private final File root; class CrawlerThread extends Thread { /
    *
    Listing 7.18
    *
    / } class IndexerThread extends Thread { /
    *
    Listing 7.19
    *
    / } public void start() { producer.start(); consumer.start();
    } public void stop() { producer.interrupt(); } public void awaitTermination() throws InterruptedException { consumer.join();
    }
    }
    Листинг 7.17 Завершение работы с помощью ядовитой пилюли public class CrawlerThread extends Thread { public void run() { try {
    crawl(root);
    } catch (InterruptedException e) { /
    *
    fall through
    *
    / } finally { while (true) { try {
    queue.put(POISON);
    break;
    } catch (InterruptedException e1) { /
    *
    retry
    *
    / }
    }
    }
    } private void crawl(File root) throws InterruptedException {
    }

    }
    Листинг 7.18 Поток производителя в классе
    IndexingService public class IndexerThread extends Thread { public void run() { try { while (true) {
    File file = queue.take();
    if (file == POISON)
    break;
    else indexFile(file);
    }
    } catch (InterruptedException consumed) { }
    }
    }
    Листинг 7.19 Поток потребителя в классе
    IndexingService
    Ядовитые пилюли работают только тогда, когда известно количество производителей и потребителей. Подход, принятый в классе
    IndexingService может быть распространен на несколько производителей, если каждый производитель будет помещать пилюлю в очередь и потребитель остановится только тогда, когда он получит таблетки от всех N
    производителей
    Также этот подход может быть распространен и на нескольких потребителей; каждый производитель должен помещать в очередь пилюли на N
    потребителей
    , хотя такая структура может стать очень громоздкой при большом количестве производителей и потребителей.
    Ядовитые пилюли надежно работают только с неограниченными очередями.
    7.2.4 Пример: одноразовая служба выполнения
    Если метод должен обработать пакет задач и не возвращать управление, пока все задачи не будет выполнены, можно упростить управление жизненным циклом службы с помощью приватного экземпляра
    Executor
    , время жизни которого ограничено временем жизни метода (методы invokeAll и invokeAny часто могут быть полезны в таких ситуациях).
    Метод checkMail в листинге 7.20 параллельно проверяет наличие свежей почты на нескольких хостах. Он создает приватный экземпляр исполнителя
    (executor) и отправляет ему по одной задаче для каждого хоста: затем он инициирует завершение работы исполнителя и ожидает завершения, которое происходит, когда все задачи проверки почты полностью выполняются
    85
    boolean checkMail(Set hosts, long timeout, TimeUnit unit) throws InterruptedException {
    ExecutorService exec = Executors.newCachedThreadPool(); final AtomicBoolean hasNewMail = new AtomicBoolean(false); try {
    85
    Причина, по которой вместо типа volatile boolean был использован тип
    AtomicBoolean заключается в том, что для доступа к флагу hasNewMail из внутреннего экземпляра
    Runnable он должен быть объявлен как final
    , что исключает его изменение.
    for (final String host : hosts) exec.execute(new Runnable() { public void run() { if (checkMail(host)) hasNewMail.set(true);
    }
    });
    } finally { exec.shutdown(); exec.awaitTermination(timeout, unit);
    } return hasNewMail.get();
    }
    Листинг 7.20 Использование приватного экземпляра
    Executor
    ,
    чей жизненный цикл ограничен вызовом метода
    7.2.5 Ограничения метода shutdownNow
    При внезапном завершении работы службы
    ExecutorService с помощью метода shutdownNow
    , она пытается отменить текущие задачи и возвращает список задач, которые были отправлены службе, но никогда не запускались, чтобы их можно было отметить в логе или сохранить для последующей обработки
    86
    . Однако, нет общего способа узнать, какие задачи были запущены, но не были завершены. Это означает, что нет способа узнать состояние выполняемых задач во время завершения работы, только если сами задачи не выполнят какую-либо контрольную точку. Чтобы узнать, какие задачи не были выполнены, нужно знать не только то, какие задачи не запускались, но и какие задачи выполнялись в момент завершения работы исполнителя
    87
    Класс
    TrackingExecutor в листинге 7.21 демонстрирует подход к определению того, какие задачи выполнялись в процессе завершения работы.
    Инкапсулируя экземпляр
    ExecutorService и инструментируя метод execute
    (а так же метод submit
    , не показанный здесь) для сохранения информации о том, какие задачи были отменены после завершения работы, класс
    TrackingExecutor может определять, какие задачи запускались, но нормально не завершились. После завершения работы исполнителя метод getCancelledTasks возвращает список отмененных задач. Для того чтобы этот метод работал, задачи, когда возвращают управление, должны сохранять статус прерывания потока, как в любом случае и ведут себя хорошо написанные задачи. public class TrackingExecutor extends AbstractExecutorService { private final ExecutorService exec; private final Set tasksCancelledAtShutdown =
    Collections.synchronizedSet(new HashSet()); public List getCancelledTasks() { if (!exec.isTerminated())
    86
    Объекты
    Runnable
    , возвращаемые методом shutdownNow
    , могут не совпадать с объектами, отправленными службе
    ExecutorService
    : они могут быть обернутыми (wrapped)экземплярами отправленных задач.
    87
    К сожалению, нет опции завершения работы, в которой задачи, еще не запущенные на выполнение, возвращались бы вызывающему объекту, но незавершенные задачи могли бы быть завершены; такая опция устранила бы это неопределенное промежуточное состояние.
    throw new IllegalStateException(...); return new ArrayList(tasksCancelledAtShutdown);
    } public void execute(final Runnable runnable) { exec.execute(new Runnable() { public void run() { try { runnable.run();
    } finally { if (isShutdown()
    && Thread.currentThread().isInterrupted()) tasksCancelledAtShutdown.add(runnable);
    }
    }
    });
    }
    // delegate other ExecutorService methods to exec
    }
    Листинг 7.21 Экземпляр
    ExecutorService отслеживающий отмененные задачи после завершения работы
    Класс
    WebCrawler
    , из листинга 7.22, демонстрирует применение класса
    TrackingExecutor
    . Работа веб-сканера часто не ограничена, поэтому, если сканер должен быть выключен, мы можем захотеть сохранить его состояние, таким образом, он может быть перезапущен позже. Класс
    CrawlTask предоставляет метод getPage
    , позволяющий узнать, с какой страницей он работает. Когда сканер завершает свою работу, то задачи на сканирование, которые не были запущены и те, что были отменены, а также их URL записываются, чтобы задачи сканирования страниц для этих URL-адресов можно было добавить в очередь при перезапуске сканера. public abstract class WebCrawler { private volatile TrackingExecutor exec;
    @GuardedBy("this") private final Set urlsToCrawl = new HashSet(); public synchronized void start() { exec = new TrackingExecutor(
    Executors.newCachedThreadPool()); for (URL url : urlsToCrawl) submitCrawlTask(url); urlsToCrawl.clear();
    } public synchronized void stop() throws InterruptedException { try { saveUncrawled(exec.shutdownNow()); if (exec.awaitTermination(TIMEOUT, UNIT)) saveUncrawled(exec.getCancelledTasks());
    } finally {
    exec = null;
    }
    } protected abstract List processPage(URL url); private void saveUncrawled(List uncrawled) { for (Runnable task : uncrawled) urlsToCrawl.add(((CrawlTask) task).getPage());
    } private void submitCrawlTask(URL u) { exec.execute(new CrawlTask(u));
    } private class CrawlTask implements Runnable { private final URL url; public void run() { for (URL link : processPage(url)) { if (Thread.currentThread().isInterrupted()) return; submitCrawlTask(link);
    }
    } public URL getPage() { return url; }
    }
    }
    Листинг 7.22 Использование класса
    TrackingExecutorService для сохранения незавершённых задач с целью последующего выполнения
    Класс TrackingExecutor содержит неизбежное условие гонки, которое может привести к ложным срабатываниям: задачи могут идентифицироваться как отмененные, но фактически будут завершены. Это происходит потому, что пул потоков может быть закрыт в момент времени между выполнением последней инструкции задачи и записью задачи как завершенной. Это не проблема, если задачи идемпотентны
    88
    (если их повторное выполнение оказывает тот же эффект, что и выполнение в первый раз), как это обычно и бывает в веб-сканере. В противном случае, приложение, извлекающее отмененные задачи, должно учитывать риск возникновения такой ситуации и быть готовым к работе с ложными срабатываниями.
    7.3 Обработка аварийного завершения потока
    Очевидно, что когда однопоточное консольное приложение завершает свою работу из-за не перехваченного исключения - программа прекращает работу и выводит в консоль трассировку стека, которая сильно отличается от типичного вывода программы. Сбой потока в параллельном приложении не всегда так очевиден.
    Трассировка стека может быть напечатана в консоли, но за консолью может никто
    88
    Идемпотентность - свойство объекта или операции при повторном применении операции к объекту давать тот же результат, что и при первом (из вики).
    не наблюдать. Кроме того, при сбое потока приложение может продолжать работать, поэтому его сбой может остаться незамеченным. К счастью, существуют средства обнаружения и предотвращения “утечки” (leaking) потоков из приложения.
    Основной причиной преждевременной смерти потока является возбуждение исключения
    RuntimeException
    . Поскольку эти исключения указывают на программную ошибку или другую неисправимую проблему, они обычно не перехватываются. Вместо этого они распространяются вверх по стеку, и в такой ситуации поведение по умолчанию заключается в печати трассировки стека в консоль и завершении потока.
    Последствия аварийного завершения потока лежат в диапазоне от “безвредно” до “катастрофа”, в зависимости от роли потока в приложении. Потеря потока из пула потоков может иметь последствия для производительности, но приложение, которое хорошо работает с пулом из 50 потоков, вероятно, будет также хорошо работать и с пулом из 49 потоков. С другой стороны, потеря потока диспетчеризующего события в приложении с графическим интерфейсом, была бы весьма заметна - приложение бы прекратило обработку событий, и в результате графический интерфейс как бы “замерз”. В классе
    OutOfTime были продемонстрированы серьезные последствия, к которым привела утечка потока: служба, представленная классом
    Timer
    , постоянно выходит из строя.
    Почти любой код может бросить исключение RuntimeException. Всякий раз, когда вы вызываете другой метод, вы верите, что метод нормально вернёт управление или бросит одно из проверяемых исключений, объявленных в его сигнатуре. Чем меньше вы знакомы с вызываемым кодом, тем более скептически вы должны относиться к его поведению.
    Потоки, обрабатывающие задачи, такие как рабочие потоки в пуле потоков или поток диспетчеризации событий Swing, проводят всю свою жизнь, вызывая неизвестный код через абстрактные барьеры, такие как экземпляры
    Runnable
    , и эти потоки должны быть очень скептически настроены по отношению к тому, что код, который они вызывают, будет хорошо себя вести. Было бы очень плохо, если бы служба, подобная потоку событий Swing, упала бы (failed) только потому, что какой-то плохо написанный обработчик событий бросил бы исключение
    NullPointerException
    . Соответственно, эти средства
    89
    должны вызывать задачи в блоке try-catch
    , который перехватывает непроверяемые исключения, или в блоке try-finally
    , чтобы гарантировать, что, если поток завершает работу аварийно, фреймворк будет проинформирован об этом и сможет предпринять корректирующие действия. Это один из немногих случаев, когда может потребоваться перехват исключения
    RuntimeException
    - при вызове неизвестного, ненадежного кода с использование некой абстракции, такой как
    Runnable
    90
    В листинге 7.23 иллюстрируется способ структурирования рабочего потока в пуле потоков. Если задача бросает непроверяемое исключение, она позволяет потоку умереть, но не ранее момента уведомления фреймворка о том, что поток умер. Затем фреймворк может принять решение о замене рабочего потока новым потоком или не заменять его, так как пул потоков уже завершил работу или уже имеется достаточное количество рабочих потоков, полностью удовлетворяющее текущий спрос. Класс
    ThreadPoolExecutor и фреймворк Swing используют этот метод, чтобы гарантировать, что плохо работающая задача не создаст препятствий
    89
    Имеются в виду потоки, обрабатывающие задачи.
    90
    До сих пор идут споры по поводу безопасности такого подхода; когда поток бросает непроверяемое исключение, всё приложение может быть скомпрометировано. Но альтернатива – завершение работы приложения - обычно непрактична.
    для выполнения последующих задач. Если вы пишете класс рабочего потока, который выполняет отправленные задачи, или вызываете ненадежный внешний код
    (например, динамически загружаемые плагины), используйте один из представленных подходов для предотвращения завершения потока, в котором происходит вызов, плохо написанной задачи или плагина. public void run() {
    Throwable thrown = null; try { while (!isInterrupted()) runTask(getTaskFromWorkQueue());
    } catch (Throwable e) { thrown = e;
    } finally { threadExited(this, thrown);
    }
    }
    Листинг 7.23 Типичная структура рабочего потока в пуле потоков
    7.3.1 Обработчики неперехваченных исключений
    В предыдущем разделе предлагалось использовать упреждающий подход к проблеме непроверяемых исключений. Поточное API также предоставляет средство
    UncaughtExceptionHandler
    , которое позволяет обнаруживать ситуацию, когда поток умирает из-за неперехваченного исключения.
    Когда поток завершает свою работу из-за неперехваченного исключения, JVM сообщает о возникновении этого события экземпляру
    UncaughtExceptionHandler,
    предоставленному приложением (см. листинг 7.24); если обработчик не существует, поведение по умолчанию будет заключаться в печати трассировки стека в поток
    System.err
    91
    public interface UncaughtExceptionHandler { void uncaughtException(Thread t, Throwable e);
    }
    Листинг 7.24 Интерфейс
    UncaughtExceptionHandler
    Что обработчик должен сделать с неперехваченным исключением, зависит от требований к качеству обслуживания
    (quality-of-service).
    Наиболее распространенным ответом является запись сообщения об ошибке и трассировки стека в лог приложения, как показано в листинге 7.25. Обработчики также могут
    91
    До Java 5.0 единственным способом управления обработчиком
    UncaughtExceptionHandler было создание подклассов ThreadGroup. В Java 5.0 и более поздних версиях можно установить обработчик
    UncaughtExceptionHandler каждому потоку с помощью метода
    Thread.setUncaughtExceptionHandler
    , а также можно установить обработчик
    UncaughtExceptionHandler по умолчанию, с помощью метода
    Thread.setDefaultUncaughtExceptionHandler
    . Однако вызван будет только один из этих обработчиков - сначала JVM ищет обработчик для каждого потока, а затем обработчик класса
    ThreadGroup
    . Реализация обработчика по умолчанию в
    ThreadGroup делегирует ответственность за обработку родительской группе потоков и так далее по цепочке до тех пор, пока один из обработчиков
    ThreadGroup не обработает неперехваченное исключение или оно не всплывет до группы потоков верхнего уровня. Обработчик группы потоков верхнего уровня делегирует обработку системному обработчику по умолчанию (если он существует; по умолчанию - нет), в противном случае выводит трассировку стека в консоль.
    выполнять и более непосредственные действия, такие как попытка перезапуска потока, завершение работы приложения, отправка сообщения оператору на пейджер
    92
    или другие корректирующие или диагностические действия. public class UEHLogger implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) {
    Logger logger = Logger.getAnonymousLogger(); logger.log(Level.SEVERE,
    "Thread terminated with exception: " + t.getName(), e);
    }
    }
    Листинг 7.25 Обработчик
    UncaughtExceptionHandler логирующий информацию об исключении
    В долговременных приложениях для всех потоков всегда используйте обработчики неперехваченных исключений, которые должны, по меньшей мере, записывать информацию о возникшем исключении в лог.
    Для того, чтобы пулу потоков установить экземпляр обработчика
    UncaughtExceptionHandler
    , предоставьте экземпляр
    ThreadFactory конструктору
    ThreadPoolExecutor
    . (Как и во всех прочих манипуляциях с потоками, только владелец потока должен изменять обработчик
    UncaughtExceptionHandler
    .) Стандартные пулы потоков позволяют брошенному задачей неперехваченному исключению завершать работу потока в пуле, но используют блок try-finally для уведомления, когда это происходит, чтобы поток можно было заменить. Без обработчика неперехваченных исключений или другого механизма уведомления о сбоях, может оказаться так, что задачи завершаются со сбоем в фоновом режиме (тихо), что может привести к путанице. Если вы хотите получать уведомления, когда задача “падает” из-за возбуждённого исключения, так что бы вы могли выполнить специфичные для задачи действия по восстановлению, либо оберните задачу с помощью экземпляров
    Runnable или
    Callable
    , которые перехватывают исключения, либо переопределите хук (hook, ловушка)
    93
    afterExecute экземпляра
    ThreadPoolExecutor
    Несколько сбивает с толку, что исключения, бросаемые задачами, превращаются в неперехваченные обработчиком исключений только для задач, отправленных с помощью метода execute
    ; для задач, отправленных с помощью submit
    , любое брошенное исключение, проверяемое оно или нет, считается частью состояния возврата задачи. Если задача, отправленная с помощью метода submit
    , завершается с исключением, исключение, обёрнутое в экземпляр
    ExecutionException
    , пробрасывается дальше с помощью метода
    Future.get
    7.4 Завершение работы JVM
    Среда JVM может завершать свою работу в плановой (orderly) или внезапной
    (abrupt) манере. Плановое завершение работы инициируется, когда последний
    “нормальный” (не демон) поток завершается, кто-то вызывает метод
    System.exit
    92
    Кто сейчас вспомнит, что это такое, а когда-то была очень полезная и распространённая штука.
    93
    Хук – метод-перехватчик, выражаясь другими словами - обработчик, срабатывающий при наступлении некоторого события.
    или другими, специфичными для платформы, средствами (например, отправка сигнала
    SIGINT или нажатие комбинации клавиш
    Ctrl-C
    ). Хотя это стандартный и предпочтительный способ завершения работы виртуальной машины Java, ее работу также можно завершить внезапно, вызвав метод
    Runtime.halt или убив процесс
    JVM средствами операционной системы (например, отправив процессу сигнал
    SIGKILL
    ).
    7.4.1 Завершающие хуки
    При плановом завершении работы, среда JVM в первую очередь запускает все зарегистрированные завершающие хуки (shutdown hooks). Завершающие хуки представляют собой незапущенные потоки, зарегистрированные с помощью метода
    Runtime.addShutdownHook
    . Среда JVM не даёт никаких гарантий относительно порядка, согласно которому будут запускаться завершающие хуки. Если потоки какого-либо приложения (демоны или не демоны) все еще выполняются в момент начала завершения работы, они продолжат выполнение параллельно с процессом завершения работы. Когда все завершающие хуки выполнятся, JVM может принять решение о выполнении финализаторов, если метод runFinalizersOnExit вернёт значение true
    , и затем остановится. Среда JVM не предпринимает никаких попыток остановить или прервать потоки приложения, которые все еще продолжают выполняться во время завершения работы; они будут резко завершены, когда JVM, в конечном счете, остановится. Если завершающие хуки или финализаторы ещё не закончили выполнение, то плановый процесс завершения работы “зависает” и работа JVM должна быть завершена внезапно. При внезапном завершении работы, от среды JVM не требуется делать ничего, кроме остановки
    JVM; завершающие хуки выполняться не будут.
    Завершающие хуки должны быть потокобезопасными: они должны использовать синхронизацию в процессе доступа к совместно используемым данным и должны заботиться о том, чтобы избегать взаимоблокировок, как, впрочем, и любой другой параллельный код. Кроме того, они не должны строить предположений о состоянии приложения (например, о том, завершили ли уже свою работу другие службы или выполнились ли все обычные потоки) или о том, почему завершает работу JVM, и поэтому должны быть закодированы с чрезвычайной степенью защиты. Наконец, они должны завершать своё выполнение как можно быстрее, так как их существование задерживает завершение работы JVM в то время, когда пользователь может ожидать, что JVM завершится быстро.
    Завершающие хуки можно использовать для очистки служб или приложений, например для удаления временных файлов или очистки ресурсов, которые автоматически не очищаются ОС. В листинге 7.26 демонстрируется, как класс
    LogService из листинга 7.16 может зарегистрировать в методе start завершающий хук, чтобы обеспечить закрытие файла журнала после завершения работы. public void start() {
    Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { LogService.this.stop(); } catch (InterruptedException ignored) {}
    }

    });
    }
    Листинг 7.26 Регистрация завершающего хука для остановки службы логирования.
    Поскольку все завершающие хуки выполняются одновременно, закрытие файла лога может привести к проблемам с другими завершающими хуками, которые также хотят использовать службу логирования. Чтобы избежать этой проблемы, завершающие хуки не должны полагаться на службы, которые могут быть закрыты приложением или другими завершающими хуками. Один из способов достигнуть этого - использовать один завершающий хук для всех служб, а не по одному хуку для каждой службы, и обязать его последовательно вызвать действия по завершению работы. Это гарантирует, что действия по завершению работы выполнятся последовательно в одном потоке, что позволит избежать возникновения условий гонки или взаимоблокировок между действиями, завершающими работу. Этот метод можно использовать независимо от того, используете ли вы завершающие хуки или нет; последовательное, а не параллельное выполнение действий по завершению работы, позволяет устранить множество потенциальных источников проблем. В приложениях, которые поддерживают явные сведения о зависимостях между службами, этот метод также может гарантировать, что действия по завершению работы выполняются в правильном порядке.
    7.4.2 Потоки демоны
    Иногда вы хотите создать поток, который выполняет некоторую вспомогательную функцию, но вы не хотите, чтобы существование этого потока препятствовало завершению работы JVM. Для этого предназначены потоки демоны (daemon
    threads).
    Потоки делятся на два типа: обычные потоки и потоки демоны. При запуске виртуальной машины Java, все создаваемые ею потоки (например, сборщик мусора и другие служебные потоки) являются потоками демонами, за исключением основного потока. При создании нового потока он наследует статус демона, от создавшего потока, поэтому по умолчанию все потоки, созданные основным потоком, также являются обычными потоками.
    Обычные потоки и потоки демоны различаются только тем, что происходит при завершении работы. Когда поток завершает работу, JVM выполняет инвентаризацию выполняющихся потоков, и если единственные оставшиеся потоки являются потоками демонами, среда инициирует упорядоченное плановое работы. Когда JVM останавливается, работа всех оставшихся потоков демонов резко прерывается, - блоки finally не выполняются, стеки не разматываются -
    JVM просто завершает свою работу.
    Потоки демоны должны использоваться крайне умеренно - выполнение нескольких выполняющихся активностей может быть прервано в любой момент времени без очистки. В частности, опасно использовать потоки демоны для задач, которые могут выполнять какие-либо операции ввода/вывода. Потоки демоны лучше всего приберегать для задач “очистки”, таких как фоновый поток, который периодически удаляет устаревшие записи из кэша в памяти.

    Потоки демоны не являются хорошим вариантом для замены обычных потоков в контексте правильного управления жизненным циклом служб приложения.
    7.4.3 Финализаторы
    Сборщик мусора выполняет полезную работу по освобождению ресурсов памяти, когда они больше не используются, но некоторые ресурсы, такие как дескрипторы файлов или сокетов, должны быть явно возвращены операционной системе, когда больше не нужны. Чтобы помочь этому процессу, сборщик мусора обрабатывает объекты, имеющие специальный нетривиальный метод finalize
    : после их освобождения сборщиком мусора, вызывается метод finalize
    , позволяющий освободить удерживаемые ресурсы.
    Поскольку финализаторы могут работать в потоке управляемом JVM, любое состояние, доступ к которому получается финализатор, будет доступно более чем в одном потоке и, следовательно, доступ к нему должен осуществляться только с использованием синхронизации. Финализаторы не предоставляют гарантий относительно момента времени, в который они будут выполнены, или даже самого факта выполнения и приводят к значительным затратам производительности на объекты обладающие нетривиальными финализаторами. Кроме того, их довольно сложно написать корректно
    94
    . В большинстве случаев сочетание блоков finally и явных вызовов методов close лучше справляется с управлением ресурсами, чем финализаторы; единственным исключением является необходимость управления объектами, содержащими ресурсы, полученные native методами. По этим и другим причинам старайтесь избегать написания или использования классов с финализаторами (кроме классов библиотеки платформы) [EJ Item 6].
    Избегайте использования финализаторов.
    7.5 Итоги
    Вопросы, касающиеся завершения жизненного цикла (end-of-lifecycle) задач, потоков, служб и приложений могут усложнить их разработку и реализацию. Язык
    Java не предоставляет упреждающего механизма для отмены выполняющихся активностей или завершения потоков. Вместо этого Java предоставляет механизм кооперативного прерывания, который можно использовать для облегчения процесса отмены, но создание протоколов для отмены и их согласованное использование зависит только от вас. Использование класса
    FutureTask и фреймворка
    Executor упрощает построение отменяемых задач и сервисов.
    94
    См. (Boehm, 2005) о некоторых проблемах, связанных с написанием финализаторов.

    1   ...   11   12   13   14   15   16   17   18   ...   34


    написать администратору сайта