Продолжаем разбор вопросов из Java-сертификации от Учебного центра IBS вместе с Игорем Судакевичем, сертифицированным Java-разработчиком, уполномоченным инструктором корпорации Oracle и платформы Udemy, Java-методистом c 15-летним опытом. В этой статье мы рассмотрим применение Threads и Executors и поможем вам подготовиться к тестированию. Допустим, вопрос касается создания потоков (также известных как «подпроцессы», threads) на базе интерфейсов Runnable и Callable. Кроме того, планируется, что управлять исполнением задач в многопоточном режиме будет объект ExecutorService. Итак: Дано: Пусть также дан следующий кодовый фрагмент: Каков результат? Выбрать два ответа: A. Severe Info Warning B. Severe Warning Info C. Severe Severe Info D. Severe Info Несмотря на то, что в вопросе отсутствуют import-директивы, это не значит, что предложенный код не скомпилируется. И если в тексте вопроса отсутствуют указания на импорты дата-типов, это значит, что все необходимые импорты на месте и код успешно компилируется и запускается. Другое дело, что на этапе исполнения могут вылетать исключения, но наш пример должен отработать штатно. На экзамен уровня «Продвинутый» вынесены подтемы из раздела Concurrency, касающиеся утилитарного класса Executors и интерфейса ExecutorService. Кроме того, экзамен активно интересуется пулами потоков, которые имплементируют интерфейс ExecutorService. В первых версиях Java на программиста возлагалась ответственность за создание и управление жизненным циклом потоков. Поскольку создание thread'ов ограничено ресурсами операционной системы, была придумана концепция пула потоков («thread pool»), чтобы набор thread'ов мог обслуживать куда больше исполняемых фоновых задач. Другими словами, вместо того чтобы для каждой задачи создавать отдельный поток и затем разрушать его, можно заранее создать несколько thread'ов и настроить их так, чтобы они могли принимать и исполнять «заказы» (например, Runnable-объектов). Поток, приостанавливающий такой «заказ», исполнит его и по завершении вернется в пул, где ему могут поручить очередной «заказ», и так далее. Готовые решения для thread-пулов появились в Java API с выходом «пятерки». Интерфейсы Executor и ExecutorService представляют собой генерализацию thread-пулов и поддерживаемых видов взаимодействий. Кроме того, ряд имплементаций ExecutorService можно инстанцировать с помощью класса Executors, где содержится набор соответствующих фабричных методов. Эти три дата-типа расположены в пакете java.util.concurrent наряду с другими высокоуровневыми классами и интерфейсами, призванными помочь разработчику с решением проблем многопоточного программирования. В то время как базовый интерфейс Executor предназначен для исполнения задач, описанных в объектах, являющихся имплементаторами интерфейса Runnable, на практике мы чаще используем ExecutorService, представляющий собой дочерний интерфейс Executor'а, поскольку ExecutorService дает нам дополнительную возможность исполнять Callable-задачи и прекращать прием задач для всего пула. Напомним, что интерфейс Callable предполагает возвращение некого результата, который будет асинхронно получен подпроцессом, являющимся инициатором исполнения задания. Интересно отметить, что ни Executor, ни ExecutorService не предписывают специфическую стратегию исполнения задач. Одни имплементации ведут многопоточную обработку в пуле фиксированного размера, приводя порой к появлению очередей из задач, поджидающих, когда наконец высвободится очередной thread. Другие имплементации умеют самостоятельно создавать рабочие потоки по мере повышения нагрузки и даже «подчищать за собой», когда необходимость в добавочных thread'ах исчезает. Есть и имплементация, где задачи исполняются строго последовательно, поскольку на все про все выделяется один-единственный thread. Какую стратегию взять, зависит от конкретной прикладной области, поэтому разработчик должен проанализировать архитектуру и потребности приложения. Вышеперечисленные стратегии реализованы в следующих фабричных методах утилитарного класса Executors: ●      newFixedThreadPool(int nThreads) ●      newCachedThreadPool() ●      newSingleThreadExecutor() Первые два создают пулы с несколькими рабочими потоками, метод newSingleThreadExecutor() возвращает сервис, который обсчитывает задачи строго по очереди в одном-единственном thread'е. Мы видим, что в нашем примере задействован кеширующий, динамический пул. Эта разновидность экзекьютора порождает новые рабочие thread'ы и разрушает их, когда поток остается незадействованным в течение 60 секунд. У такого пула есть и недостаток: отсутствие потолка для числа создаваемых thread'ов. Это может вызвать чрезмерное потребление ресурсов и, как следствие, деградацию эксплуатационных характеристик приложения под высокой нагрузкой. У нас есть целый ряд задач для обсчета и есть основания ожидать, что пул из нашего вопроса будет содержать несколько thread'ов в многопоточном режиме. Вот почему отсутствует гарантия, что задача будет досчитана первой по счету, второй и так далее, причем вне зависимости от порядка их запуска на исполнение. Другими словами, невозможно заранее сказать, в какой последовательности будут печататься сообщения в консоли. Отсюда следует, что правильными ответами будут варианты A и B. Отметим, что ExecutorService выполняет порученную задачу однократно. Мы не можем исключить, что исполнение закончится аварийно или задача вообще не поступит на обработку, потому что пул к этому времени окажется закрыт. Однако обсчет задачи происходит лишь один раз. Мы не увидим дублированных сообщений, поэтому вариант C является ошибочным. Теперь поподробнее о методе shutdown(). После его вызова ExecutorService не станет принимать новые поручения, но запущенные задачи будут досчитаны до конца. Отсюда получаем, что, если мы поручили пулу три задачи и затем вызвали shutdown(), в случае штатного завершения в консоли появятся все три сообщения. Поскольку вариант D перечисляет только два сообщения и не упоминает вылет какого-либо исключения, мы приходим к выводу, что опция D тоже является ошибочной. А что, если какая-то задача обсчитывается настолько долго, что мы вылетим за границу таймаута в 15 секунд после требования закрыть пул? Ведь в этом случае мы не увидим все три сообщения, верно? В этом случае, во-первых, вообще крайне мала вероятность, что простенькая задача вывести сообщение в консоль потребует столь долгого времени. Во-вторых, вопрос просил указать два варианта, и опции A и B выглядят абсолютно естественными, не требуют никаких натяжек, оговорок и экзотических корнер-кейсов с микроскопически малыми шансами для реализации. К такому сценарию мы можем отнести ситуацию, когда ядро операционной системы на сервере обрывает работу виртуальной машины из-за сверхсрочной необходимости установить некий апдейт или патч безопасности в условиях отчаянного дефицита ресурсов, но на нашем экзамене все эти обстоятельства были бы описаны в явном виде. Поскольку любой поток в thread-пуле является недемоническим, виртуальная машина не может по своей воле оборвать его исполнение принудительно. Вот почему программа должна отработать до конца и вывести в консоль все три сообщения, пусть даже в непредсказуемом порядке.