Java EE Concurrency API

    Всем привет!

    А мы тут плюшками балуемся запускаем второй поток курса «Разработчик Java Enterprise». Бессменный создатель и преподаватель курса — Виталий Иванов, написал вот по этому поводу статью даже, которая, как надеемся, покажется вам полезной :)

    Так что поехали :)

    Данная статья посвящена изучению API к спецификации JavaEE Concurrency (JSR 236), определяющий стандарт выполнения параллельных задач в JavaEE контейнере, используя концепцию управляемых ресурсов. Выход седьмой версии JavaEE сделал возможным запуск параллельных задач в Enterprise-контейнерах, предоставляя разработчику удобные средства и утилиты для работы с многозадачностью. До того самого момента, вся многозадачность отдавалась на откуп конкретной реализации используемого сервера приложений, самостоятельно принимающего решение об оптимизации выполнения задач. Нарушение этого принципа расценивалось как плохая практика построения архитектуры Enterprise-приложений. Вследствие чего, разработчику не рекомендовалось создавать новые потоки, а иногда и запрещалось подобное поведение на уровне контейнера.


    The enterprise bean must not attempt to manage threads. The enterprise bean must not attempt to start, stop, suspend, or resume a thread, or to change a thread’s priority or name. The enterprise bean must not attempt to manage thread groups.

    (свободный перевод автора: EJB не должны пытаться управлять потоками, а именно пытаться запускать, останавливать, приостанавливать, а также восстанавливать их выполнение или же менять приоритет или изменять имя потока. Также EJB не должны пытаться управлять группами потоков).

    По факту, запретить создание своих собственных потоков в JavaEE-контейнерах достаточно проблематично, однако при таком подходе «фоновые» службы контейнера не могут гарантировать корректность выполнения своей работы. Например, закрытие транзакции по завершению метода EJB потенциально могло бы работать некорректно в случае запуска задач в новом потоке, используя наследников Threads (или имплементаций Runnable) из JavaSE. Также использование базовых интерфейсных типов из поставки Executor API такие, как ExecutorService и ScheduledExecutorService, при создании через статичные методы класса Executors, приводило бы к потенциальным ошибкам и нарушению порядка выполнения служб контейнера.

    Из рекомендованных спецификацией JavaEE средств для асинхронного выполнения задач в распоряжении разработчика оставалось использование асинхронных Stateless/Statefull EJB и/или Message Driven бинов, возможностей которых достаточно для определенного круга задач и самое главное, что управление которыми изначально целиком и полностью находится под контролем сервера приложений, а именно EJB-контейнера.

    Однако, как уже отмечалось ранее, благодаря JSR 236, появились управляемые контейнером ресурсы, реализующие поддержку многопоточности и асинхронного выполнения задач, расширяя возможности пакета java.util.concurrent из JavaSE. Для стека JavaEE классы управляемых ресурсов располагаются в пакете javax.enterprise.concurrent, при этом доступ к объектам этих классов осуществляется через внедрение ресурса, используя аннотацию @Resource, или через JNDI-контекст (в частности, InitialContext). Вместе с тем, добавились возможности использования привычных для многопоточной среды объектов Future/ScheduledFuture/CompletableFuture внутри приложений JavaEE.

    Итак, довольно лирики и давайте уже подробнее рассмотрим каждый из предоставляемых спецификацией управляемых ресурсов с практической точки зрения, а именно — в разрезе использования в прикладном коде приложения, а также с точки зрения конфигурирования ресурсов на примере сервера приложений Glassfish 5.

    Что ж, первым на очереди рассмотрения выбран класс ManagedExecutorService, который (уже понимая из названия) расширяет возможности привычного ExecutorService «из коробки» JavaSE и предназначенного для асинхронного выполнения задач в среде JavaEE.

    Для конфигурирования в рамках сервера приложений Glassfish не только данного типа ExecutorService следует обратиться к конфигурационному файлу domain.xml, расположение которого определяется каталогом ${GLASSFISH_HOME}/domains/<имя_домена>/config. Фрагмент данного файла представлен ниже:

    <domain application-root="${com.sun.aas.instanceRoot}/applications" version="25" log-root="${com.sun.aas.instanceRoot}/logs">
        <resources>
            <context-service object-type="system-all" jndi-name="concurrent/__defaultContextService" />
            <managed-executor-service object-type="system-all" jndi-name="concurrent/__defaultManagedExecutorService" />
            <managed-scheduled-executor-service object-type="system-all" jndi-name="concurrent/__defaultManagedScheduledExecutorService" />
            <managed-thread-factory object-type="system-all" jndi-name="concurrent/__defaultManagedThreadFactory" />
        </resources>
        <servers>
            <server config-ref="server-config" name="server">
                <resource-ref ref="concurrent/__defaultContextService" />
                <resource-ref ref="concurrent/__defaultManagedExecutorService" />
                <resource-ref ref="concurrent/__defaultManagedScheduledExecutorService" />
                <resource-ref ref="concurrent/__defaultManagedThreadFactory" />
            </server>
        </servers>
    </domain>

    Заходя в интерфейс администраторской панели Glassfish 5, конфигурирование

    ManagedExecutorService выглядит следующим образом:



    В данном разделе допускается создание новых однотипных ресурсов, управление имеющимися, удаление, а также блокировка и разблокировка.

    Для любителей консольного администрирования в Glassfish представлена мощная утилита asadmin, используя в которой команду create-managed-executor-service, можно создавать новые ресурсы ManagedExecutorService:



    В прикладном коде для получения ссылки на объект созданного ManagedExecutorService удобнее использовать внедрение ресурса, но также можно воспользоваться средствами JNDI, как показано ниже:

    @Resource(lookup = "concurrent/OtusExecutorService")
    ManagedExecutorService executor; 
    
    InitialContext context = new InitialContext();
    ManagedExecutorService managedExecutorServiceWithContext = 
            (ManagedExecutorService) context.lookup(
                    "concurrent/OtusExecutorService"); 
    

    Хотелось бы обратить внимание читателя, что для аннотации @Resource параметр lookup является опциональным и если он не определен разработчиком в коде приложения, то контейнер инжектирует умолчательные ресурсы, в имени которых присутствует префикс __default. В таком случае, для разработчика код становится еще лаконичнее:

    @Resource
    ManagedExecutorService executor; 

    После получения ссылки на данный объект, используя методы execute() и submit(), можно внутри контейнера запускать задачи, имплементирующие интерфейс Runnable или Callable.

    Переходя к примеру, хочется отметить, что среди всего многообразия возможных кейсов, наибольший интерес представляют задачи, выполняемые в распределенной среде JavaEE и в которых важно обеспечить поддержку транзакционности в многопоточной среде. Как известно, в JavaEE разработана спецификация JTA (Java Transaction API), которая позволяет определять границы транзакции, явно начиная её методом begin() и завершая методами commit(), фиксируя изменения, или rollback(), откатывающий произведенные действия.

    Рассмотрим пример таска, который возвращает сообщение из некоторого списка, состоящего из ста элементов, по индексу в рамках пользовательской транзакции:

    public class TransactionSupportCallableTask implements Callable<String> {
    
        private int messageIndex;
    
        public TransactionSupportCallableTask(int messageId) {
            this. messageIndex = messageId;
        }
    
        public String call() {
            UserTransaction tx = lookupUserTransaction();
            String message = null;
            try {
                tx.begin();
                message = getMessage(messageIndex);
                tx.commit();
            } catch (Exception e) {
                e.printStackTrace();
                try {
                    tx.rollback();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
            return message;
        }
    
        private void getMessage(int index) { … }
    
        private UserTransaction lookupUserTransaction () { … }
    
    }

    Код сервлета, выводящего сообщение из списка по случайно выбранному индексу:

    @WebServlet("/task")
    public class ManagedExecutorServiceServlet extends HttpServlet {
    
        @Resource(lookup = "concurrent/OtusExecutorService")
        ManagedExecutorService executor;
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            Future<String> futureResult = executor.submit(new TransactionSupportCallableTask(Random.nextInt(100)));
            while (!futureResult.isDone()) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                response.getWriter().write("Callable task has received message with following content '" + futureResult.get() + "'");
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

    Следующим на рассмотрение выбран ресурс ManagedScheduledExecutorService, основное предназначение которого заключается в планировании задач, повторяющихся с некоторой периодичностью или требующих отложенного выполнения.



    С точки зрения конфигурирования данного ресурса через администраторскую консоль GlassFish, в сравнении с предыдущим типом особых изменений не обнаружено:



    Для быстрого создания ресурса с типом ManagedScheduledExecutorService утилита asadmin обладает командой create-managed-scheduled-executor-service



    В прикладном коде по-прежнему используем внедрение ресурса:

    @Resource(lookup = "concurrent/OtusScheduledExecutorService")
    ManagedScheduledExecutorService scheduledExecutor;

    Основными методами выполнения задач для данного типа ExecutorService являются schedule(), принимающий на вход задачи типа Runnable или Callable, и scheduleAtFixedRate(), дополнительно определяющий первоначальную задержку в выполнении задачи и задающего интервал повторений в TimeUnit-ах (секундах, минутах и т.д.).

    Предыдущий кейс можно переписать следующим образом:

    @WebServlet("/scheduledTask")
    public class ManagedScheduledExecutorServiceServlet extends HttpServlet {
    
        @Resource(lookup = "concurrent/OtusScheduledExecutorService")
        ManagedScheduledExecutorService scheduledExecutor;
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            ScheduledFuture<String> futureResult = scheduledExecutor.schedule(
                    new TransactionSupportCallableTask(Random.nextInt(100)), 5, TimeUnit.SECONDS);
            while (!futureResult.isDone()) {
                try {
                    Thread.sleep(50); // Wait
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                response.getWriter().write("Callable task received message with following content '" + futureResult.get() + "'");
            } catch ( Exception e) {
                e.printStackTrace();
            }
        }
    }

    Также в Concurrency API для Enterpise-среды предоставляется возможность создания управляемых потоков. Для этих задач следует пользоваться возможностями управляемой фабрики потоков, реализующей свой функционал через одноименный класс ManagedThreadFactory и доступ к которой также осуществляется через службу JNDI:

    @Resource
    ManagedThreadFactory factory;

    Окно администрирования консоли Glassfish выглядит «по старинке»:



    Используя управляемую фабрику потоков, появляется возможность не только предоставить контейнеру механизмы контроля потоками, но и инициализировать свойства порождаемых потоков: задавать имена и определять приоритеты, что в последующем может серьезно упростить поиск проблем при разборе дампа нитей, легко обнаружив порядок выполнения ранее именнованых потоков.

    В нашем случае, определим класс потока, выводящего информацию о друге, с которым неразрывно связан данный таск, в консоль:

    public class SimpleThreadTask implements Runnable {
    
        private String friend;
    
        public SimpleThreadTask(String friend){
            this.friend = friend;
        }
    
        @Override
        public void run() {
            System.out.println("Hello, " + friend);
        }
    }

    Пусть сервлет запускает поток и сообщает об этом на выходе:

    @WebServlet("/thread")
    public class ManagedThreadFactoryServlet extends HttpServlet {
    
        @Resource
        ManagedThreadFactory factory;
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            Thread thread = factory.newThread(new SimpleThreadTask("Otus"));
            thread.setName("ManagedThreadFromPool");
            thread.setPriority(7);
            thread.start();
            response.getWriter().write("Custom thread has been running.");
        }
    }
    

    Переходя к заключительной возможности JavaEE в области многопоточности – Context Services, следует заметить, что благодаря данным сервисам создаются динамические контекстные прокси-объекты. Всем нам прекрасно знакомы возможности динамических прокси из JavaSE (java.lang.reflect.Proxy), позволяющие генерировать динамическую реализацию требуемых интерфейсов, чьи возможности активно используются для задач создания соединений с БД и управления транзакциями, задействуются для всевозможных АОП-перехватчиков и прочее. Более того, для прокси, создаваемых посредством контекстных сервисов JavaEE, предполагается возможность работы в рамках общего JNDI контекста, контекста безопасности и класслоудера контейнера.

    Для подключения сервиса достаточно использовать код:

    @Resource
    ContextService service;

    С точки зрения администрирования и конфигурирования данного ресурса все крайне знакомо и схоже с уже рассмотренными типами:



    Ниже приведем пример потока, запускающего задачу-прокси в контексте контейнера:

    public class SampleProxyTask implements Runnable {
    
        @Override
        public void run() {
    	//контекст контейнера
            Subject subject = Subject.getSubject(AccessController.getContext());
            logInfo(subject.getPrincipals()); //логируем информацию о принципалах
            calculateSmth();
        }
    
        private void calculateSmth() { … }
    
        private void logInfo(Set<Principal> subject) { … }
    }

    EJB-бин без сохранения состояния для создания контекстных прокси:

    @Stateless
    public class ContextServiceBean {
    
        @Resource
        ContextService service;
    
        @Resource
        ManagedExecutorService executor;
    
        public void perform(Runnable task) {
            Runnable proxy = service.createContextualProxy(task, Runnable.class);
            executor.submit(proxy);
        }
    }

    И наконец код сервлета, выполняющего задачу:

    @WebServlet("/context")
    public class ContextServiceServlet extends HttpServlet {
    
        @Inject
        ContextServiceBean contextServiceBean;
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            contextServiceBean.perform(new SampleProxyTask());
        }
    }

    На этом собственно и заканчиваются возможности разработчика JavaEE для работы в многопоточной среде. Благодаря ним, все процессы и службы, протекающие в контейнере, будут находит под четким контролем сервера, согласовывая свою работу и не нарушая привычный порядок выполнения. Для целевых задач Enterprise-разработчика зачастую этих возможностей хватает и в восьмой версии данный API не претерпел изменений.

    THE END

    Как всегда ждём вопросы и комментарии и обязательно загляните к Виталию на открытый урок, там ему тоже можно позадавать вопросы и прослушать\поучаствовать в теме «CDI in action@
    • +15
    • 4,5k
    • 2
    OTUS. Онлайн-образование
    506,00
    Авторские онлайн‑курсы для профессионалов
    Поделиться публикацией

    Комментарии 2

      0
      Я правильно понял, что всю статью можно уместить в утверждение: создаём пул потоков, кладём в JNDI, достём его в коде и используем для запуска Runnable/Callable задач. И всё это нужно только для того, чтобы сервер мог вызвать shutdown пулу?
        0
        Статья, собственно, немного про другое: она подсвечивает основные средства, предоставляемые платформой JavaEE для работы с многозадачностью.
        Управление и конфигурирование пулами потоков сосредоточено в одном месте и прикладному разработчику действительно остается лишь воспользоваться предоставляемыми Application-сервером ресурсами.
        Собственно за эту простоту и хочется выразить особую благодарность спецификации, которая снимает головную боль разработчика по управлению жизненным циклом такого рода ресурсами.
        При этом нельзя забывать, что работая в Enterprise-среде, желание написать свой велосипед может привести к неожиданным последствиям и результатам от выполняемых сервером фоновых процессов.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое