Асинхронные сервлеты в Java: повышаем производительность приложений
Для кого эта статья:
- Java-разработчики, желающие улучшить свои навыки в создании высокопроизводительных веб-приложений
- Специалисты в области корпоративной разработки, заинтересованные в оптимизации процессов обработки запросов
Архитекторы систем и технические лидеры, ищущие современные подходы к масштабируемости и эффективности серверных решений
Высоконагруженные веб-приложения требуют особого подхода к обработке запросов. Синхронная модель сервлетов, при которой один поток обслуживает один запрос от начала до конца, становится узким местом при масштабировании. Асинхронные сервлеты в Java позволяют разорвать эти цепи ограничений, высвобождая потоки контейнера для обработки новых запросов, пока длительные операции выполняются в фоне. Этот подход радикально повышает пропускную способность приложений и эффективность использования ресурсов сервера – ключевые факторы успеха в мире корпоративной разработки. 🚀
Мечтаете создавать высокопроизводительные веб-приложения на Java, способные выдерживать серьезные нагрузки? На Курсе Java-разработки от Skypro вы научитесь применять асинхронные сервлеты и другие продвинутые техники оптимизации. Преподаватели-практики расскажут не только о теории, но и поделятся реальными кейсами из своего опыта в enterprise-разработке. После курса вы сможете проектировать архитектуру, выдерживающую миллионы запросов, что мгновенно повысит вашу ценность как специалиста.
Асинхронные сервлеты в Java: концепция и преимущества
Асинхронные сервлеты представляют собой эволюционный шаг в развитии Servlet API, введенный в спецификации Servlet 3.0. Их главное отличие от традиционных синхронных сервлетов заключается в способности освобождать поток контейнера во время выполнения длительных операций, таких как запросы к внешним сервисам, обращение к базам данных или сложные вычисления.
При использовании стандартных (синхронных) сервлетов поток, обрабатывающий запрос, блокируется до завершения всей работы. Это означает, что веб-сервер вынужден поддерживать большое количество активных потоков, которые большую часть времени просто ожидают завершения операций ввода-вывода, что приводит к неэффективному использованию ресурсов.
Асинхронные сервлеты решают эту проблему, предоставляя механизм, позволяющий:
- Освободить поток контейнера, сохранив контекст запроса для последующей обработки
- Делегировать выполнение длительных операций другим потокам
- Возобновить обработку запроса после завершения асинхронных операций
- Значительно увеличить количество одновременно обрабатываемых запросов
| Характеристика | Синхронные сервлеты | Асинхронные сервлеты |
|---|---|---|
| Использование потоков контейнера | Блокируется на все время обработки | Освобождается после инициации асинхронной обработки |
| Максимальное количество запросов | Ограничено размером пула потоков | Может значительно превышать размер пула потоков |
| Эффективность при I/O операциях | Низкая (ресурсы простаивают) | Высокая (ресурсы используются эффективно) |
| Сложность реализации | Низкая | Средняя/Высокая |
Ключевые преимущества асинхронных сервлетов:
- Улучшенная масштабируемость — возможность обслуживать больше клиентских запросов с меньшим количеством потоков
- Оптимизация ресурсов — эффективное использование потоков контейнера и памяти сервера
- Повышенная отзывчивость — сервер продолжает обрабатывать новые запросы даже при наличии длительных операций
- Защита от перегрузки — снижение вероятности исчерпания пула потоков при пиковых нагрузках
- Интеграция с современными паттернами — естественное встраивание в реактивные и событийно-ориентированные архитектуры
Андрей Соколов, архитектор высоконагруженных систем
Два года назад мы столкнулись с критической проблемой в нашем платежном шлюзе. Система обрабатывала около 200 транзакций в секунду, но при интеграции с новым медленным провайдером мы начали наблюдать периодические отказы сервиса. Увеличение пула потоков лишь усугубляло ситуацию, приводя к чрезмерному потреблению памяти и CPU.
Решение пришло в виде рефакторинга с использованием асинхронных сервлетов. Мы изолировали длительные операции с внешними API в отдельные обработчики и применили механизм AsyncContext. Результаты оказались впечатляющими: количество используемых потоков снизилось на 70%, пропускная способность выросла на 40%, а средняя задержка при пиковых нагрузках упала с 2100 мс до 780 мс.
Самое удивительное, что нам не пришлось увеличивать вычислительные ресурсы — производительность выросла исключительно за счет более эффективного использования существующей инфраструктуры.

Настройка среды для разработки асинхронных сервлетов
Для работы с асинхронными сервлетами необходимо правильно настроить среду разработки, включая выбор подходящих инструментов и конфигурацию проекта. Следуя этим шагам, вы создадите основу для разработки высокопроизводительных веб-приложений на Java.
Прежде всего, убедитесь, что используете совместимые версии компонентов:
- Java 8 или выше (рекомендуется Java 11+ для доступа к улучшенным возможностям многопоточности)
- Servlet API 3.0+ (обязательное требование для асинхронной обработки)
- Servlet-контейнер с поддержкой Servlet 3.0+ (Tomcat 7+, Jetty 8+, WildFly, GlassFish 4+)
- Система сборки (Maven или Gradle) для управления зависимостями
Настройка Maven-проекта для работы с асинхронными сервлетами:
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Опционально: для улучшенной работы с асинхронностью -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.44.v20210927</version>
<scope>test</scope>
</dependency>
<!-- Для тестирования асинхронных компонентов -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Настройка web.xml для поддержки асинхронной обработки:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>AsyncServlet</servlet-name>
<servlet-class>com.example.AsyncServletExample</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>AsyncServlet</servlet-name>
<url-pattern>/async</url-pattern>
</servlet-mapping>
</web-app>
Важные аспекты конфигурации среды:
- Настройка таймаутов: По умолчанию асинхронный запрос имеет ограниченное время жизни (обычно 30 секунд). Для длительных операций необходимо увеличить этот параметр.
- Пул потоков: Рекомендуется создать отдельный пул потоков для асинхронных операций, чтобы не конкурировать с потоками контейнера.
- Логирование: Настройте детальное логирование асинхронных операций для отладки сложных сценариев.
- Мониторинг: Интегрируйте инструменты для отслеживания состояния асинхронных запросов.
| Параметр контейнера | Tomcat | Jetty | Рекомендуемое значение |
|---|---|---|---|
| Асинхронный таймаут | asyncTimeout | maxIdleTime | 60000 мс (для большинства API-интеграций) |
| Максимальный размер пула | maxThreads | maxThreads | 200-500 (зависит от характера нагрузки) |
| Очередь запросов | acceptCount | acceptQueueSize | 100-500 (зависит от пиковой нагрузки) |
| Тайм-аут соединения | connectionTimeout | idleTimeout | 20000 мс (для защиты от медленных клиентов) |
Для более продвинутых сценариев рассмотрите использование специализированных библиотек, упрощающих работу с асинхронностью:
- CompletableFuture (встроен в Java 8+) для композиции асинхронных операций
- RxJava для реактивного программирования
- Executor Framework для управления пулами потоков
- Spring WebFlux для полностью реактивного подхода (альтернативный путь)
Создание базового асинхронного сервлета: пошаговый код
Теперь перейдем от теории к практике и создадим базовый асинхронный сервлет. Этот пример демонстрирует основные принципы и шаблоны работы с асинхронной обработкой в Java Servlet API. 🖥️
Шаг 1: Создайте класс сервлета с поддержкой асинхронной обработки
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = "/async-example", asyncSupported = true)
public class BasicAsyncServlet extends HttpServlet {
// Создаем выделенный пул потоков для асинхронных задач
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Получаем асинхронный контекст
final AsyncContext asyncContext = request.startAsync();
// Опционально: устанавливаем таймаут (в миллисекундах)
asyncContext.setTimeout(30000);
// Передаем асинхронную задачу в пул потоков
executorService.submit(() -> processAsyncRequest(asyncContext));
// Метод doGet возвращается немедленно, освобождая поток контейнера
System.out.println("Servlet thread released");
}
private void processAsyncRequest(AsyncContext asyncContext) {
try {
// Имитация длительной операции
System.out.println("Long-running task started");
Thread.sleep(5000);
System.out.println("Long-running task completed");
// Получаем response из контекста
HttpServletResponse response =
(HttpServletResponse) asyncContext.getResponse();
// Устанавливаем заголовки и тип контента
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
// Записываем ответ
response.getWriter().write("Async processing completed at: " +
new java.util.Date());
} catch (Exception e) {
System.err.println("Error in async processing: " + e.getMessage());
} finally {
// Важно: завершаем асинхронную обработку
asyncContext.complete();
}
}
@Override
public void destroy() {
executorService.shutdown();
super.destroy();
}
}
Шаг 2: Создайте слушателя для отслеживания жизненного цикла асинхронного запроса
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import java.io.IOException;
public class CustomAsyncListener implements AsyncListener {
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("Async request completed");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("Async request timed out");
// Обработка случая превышения времени ожидания
event.getAsyncContext().getResponse().getWriter()
.write("Operation timed out after " +
event.getAsyncContext().getTimeout() + " ms");
event.getAsyncContext().complete();
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("Error in async request: " + event.getThrowable().getMessage());
// Обработка ошибки
event.getThrowable().printStackTrace();
event.getAsyncContext().complete();
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("Async request started");
}
}
Шаг 3: Расширьте сервлет для использования слушателя
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Получаем асинхронный контекст
final AsyncContext asyncContext = request.startAsync();
// Устанавливаем таймаут
asyncContext.setTimeout(30000);
// Добавляем слушателя событий
asyncContext.addListener(new CustomAsyncListener());
// Передаем асинхронную задачу в пул потоков
executorService.submit(() -> processAsyncRequest(asyncContext));
System.out.println("Servlet thread released");
}
Шаг 4: Добавьте обработку ошибок и правильное завершение ресурсов
private void processAsyncRequest(AsyncContext asyncContext) {
HttpServletResponse response = null;
try {
// Имитация длительной операции
System.out.println("Long-running task started");
Thread.sleep(5000);
System.out.println("Long-running task completed");
// Получаем response из контекста
response = (HttpServletResponse) asyncContext.getResponse();
// Проверка, не был ли запрос уже завершен (например, по таймауту)
if (asyncContext.getRequest().isAsyncStarted()) {
// Устанавливаем заголовки и тип контента
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
// Записываем ответ
response.getWriter().write("Async processing completed at: " +
new java.util.Date());
}
} catch (Exception e) {
System.err.println("Error in async processing: " + e.getMessage());
// Обработка ошибки, если ответ еще не отправлен
if (response != null && asyncContext.getRequest().isAsyncStarted()) {
try {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("Error occurred: " + e.getMessage());
} catch (IOException ioe) {
System.err.println("Failed to write error response: " + ioe.getMessage());
}
}
} finally {
// Завершаем асинхронную обработку, если она еще активна
if (asyncContext.getRequest().isAsyncStarted()) {
asyncContext.complete();
}
}
}
Ключевые моменты, которые необходимо понять:
- AsyncContext — главный объект, управляющий жизненным циклом асинхронного запроса
- request.startAsync() — инициирует асинхронную обработку и освобождает поток контейнера
- setTimeout() — устанавливает максимальное время ожидания ответа
- asyncContext.complete() — сигнализирует о завершении обработки запроса
- AsyncListener — обеспечивает мониторинг и обработку событий жизненного цикла
- ExecutorService — предоставляет выделенный пул потоков для асинхронных задач
Дмитрий Волков, ведущий Java-разработчик
На одном из проектов мы столкнулись с необходимостью интеграции с медленным сторонним API прогноза погоды. Запросы занимали до 10 секунд, что негативно сказывалось на отзывчивости всего приложения.
Изначально мы реализовали кэширование, но это не решало проблему "холодных" запросов. Когда я внедрил асинхронные сервлеты, результат превзошел ожидания. Вместо реализации, показанной в примере выше, мы пошли дальше и добавили CompletableFuture для более элегантной обработки результатов:
JavaСкопировать код@WebServlet(urlPatterns = "/weather", asyncSupported = true) public class WeatherServlet extends HttpServlet { private final WeatherService weatherService = new WeatherService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String city = request.getParameter("city"); final AsyncContext asyncContext = request.startAsync(); CompletableFuture.supplyAsync(() -> weatherService.getWeatherForCity(city)) .thenAccept(weather -> { try { HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse(); resp.setContentType("application/json"); resp.getWriter().write(weather.toJson()); } catch (IOException e) { throw new CompletionException(e); } finally { asyncContext.complete(); } }) .exceptionally(ex -> { try { HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse(); resp.setStatus(500); resp.getWriter().write("Error: " + ex.getMessage()); } catch (IOException e) { e.printStackTrace(); } finally { asyncContext.complete(); } return null; }); } }Нагрузочное тестирование показало, что наш сервер теперь мог обрабатывать в 8 раз больше одновременных запросов при той же конфигурации аппаратных ресурсов. Это позволило нам избежать горизонтального масштабирования и сэкономить на инфраструктуре.
Обработка длительных операций с асинхронными методами
Длительные операции — главная причина использования асинхронных сервлетов. Под длительными понимаются операции, которые занимают значительное время и не требуют постоянного использования потока контейнера. Рассмотрим эффективные стратегии их обработки с применением современных возможностей Java. ⏱️
Существует несколько типов длительных операций, с которыми регулярно сталкиваются разработчики:
- Запросы к внешним API и веб-сервисам
- Сложные запросы к базам данных
- Операции обработки файлов (особенно больших)
- Ресурсоемкие вычисления
- Агрегация данных из нескольких источников
Ключевой принцип эффективной асинхронной обработки — делегирование длительных операций отдельным потокам или механизмам без блокирования потока контейнера. Рассмотрим несколько подходов:
1. Использование ExecutorService для выполнения задач
@WebServlet(urlPatterns = "/async-executor", asyncSupported = true)
public class ExecutorAsyncServlet extends HttpServlet {
private final ExecutorService executorService = Executors.newFixedThreadPool(20);
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(60000); // 60 секунд
executorService.execute(() -> {
try {
// Имитация сложного запроса к базе данных
Thread.sleep(3000);
// Получение данных
String result = fetchDataFromDatabase();
HttpServletResponse asyncResponse =
(HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("application/json");
asyncResponse.getWriter().write(result);
} catch (Exception e) {
log("Error in async processing", e);
} finally {
asyncContext.complete();
}
});
}
private String fetchDataFromDatabase() {
// Здесь был бы реальный запрос к БД
return "{\"status\":\"success\",\"data\":[1,2,3]}";
}
@Override
public void destroy() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
super.destroy();
}
}
2. Применение CompletableFuture для композиции асинхронных операций
CompletableFuture — мощный инструмент для работы с асинхронностью, введенный в Java 8. Он позволяет создавать цепочки асинхронных операций с элегантной обработкой ошибок.
@WebServlet(urlPatterns = "/async-completable", asyncSupported = true)
public class CompletableFutureServlet extends HttpServlet {
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
private final ExternalServiceClient serviceClient = new ExternalServiceClient();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final String userId = request.getParameter("userId");
final AsyncContext asyncContext = request.startAsync();
// Создаем цепочку асинхронных операций
CompletableFuture.supplyAsync(() -> serviceClient.getUserProfile(userId), executorService)
.thenCombine(
CompletableFuture.supplyAsync(() -> serviceClient.getUserOrders(userId), executorService),
(profile, orders) -> {
// Объединяем результаты двух параллельных запросов
Map<String, Object> result = new HashMap<>();
result.put("profile", profile);
result.put("orders", orders);
return result;
}
)
.thenAccept(result -> {
try {
HttpServletResponse asyncResponse =
(HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("application/json");
// В реальном приложении использовали бы Jackson или Gson
asyncResponse.getWriter().write(result.toString());
} catch (IOException e) {
throw new CompletionException(e);
} finally {
asyncContext.complete();
}
})
.exceptionally(ex -> {
try {
HttpServletResponse asyncResponse =
(HttpServletResponse) asyncContext.getResponse();
asyncResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
asyncResponse.getWriter().write("Error: " + ex.getMessage());
} catch (IOException e) {
log("Failed to send error response", e);
} finally {
asyncContext.complete();
}
return null;
});
}
// Имитация клиента внешнего сервиса
static class ExternalServiceClient {
public Map<String, Object> getUserProfile(String userId) {
// Имитация внешнего вызова
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Map<String, Object> profile = new HashMap<>();
profile.put("id", userId);
profile.put("name", "User " + userId);
return profile;
}
public List<String> getUserOrders(String userId) {
// Имитация внешнего вызова
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return Arrays.asList("Order1", "Order2", "Order3");
}
}
@Override
public void destroy() {
executorService.shutdown();
super.destroy();
}
}
3. Обработка событий, инициируемых внешними системами (Server-Sent Events)
Асинхронные сервлеты отлично подходят для реализации потоковой передачи данных (Server-Sent Events), когда сервер отправляет обновления клиенту в режиме реального времени.
@WebServlet(urlPatterns = "/stream-data", asyncSupported = true)
public class StreamingServlet extends HttpServlet {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync();
// Настройка для SSE
HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("text/event-stream");
asyncResponse.setCharacterEncoding("UTF-8");
asyncResponse.setHeader("Cache-Control", "no-cache");
// Счетчик для демонстрации
final AtomicInteger counter = new AtomicInteger(0);
// Планируем периодическую отправку данных
final ScheduledFuture<?> task = scheduler.scheduleAtFixedRate(() -> {
try {
if (counter.get() >= 10) {
asyncContext.complete();
return;
}
PrintWriter writer = asyncResponse.getWriter();
writer.write("data: " + new Date() + " – Update " +
counter.incrementAndGet() + "\n\n");
writer.flush();
} catch (IOException e) {
asyncContext.complete();
}
}, 0, 1, TimeUnit.SECONDS);
// Добавляем слушателя для отмены задачи при прерывании соединения
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) {
task.cancel(false);
}
@Override
public void onTimeout(AsyncEvent event) {
task.cancel(false);
}
@Override
public void onError(AsyncEvent event) {
task.cancel(false);
}
@Override
public void onStartAsync(AsyncEvent event) {}
});
}
@Override
public void destroy() {
scheduler.shutdown();
super.destroy();
}
}
Сравнение подходов к обработке длительных операций:
| Подход | Преимущества | Недостатки | Лучшее применение |
|---|---|---|---|
| ExecutorService | Простота, полный контроль над жизненным циклом потоков | Ограниченные возможности композиции, ручная обработка ошибок | Простые, независимые асинхронные операции |
| CompletableFuture | Декларативный стиль, мощная композиция, встроенная обработка ошибок | Более сложный код, крутая кривая обучения | Сложные цепочки операций, комбинирование результатов нескольких источников |
| Server-Sent Events | Потоковая передача в реальном времени, стандартизированный протокол | Однонаправленная связь, ограничения в некоторых браузерах | Оповещения в реальном времени, потоковые данные, метрики |
Рекомендации по обработке длительных операций:
- Всегда устанавливайте таймауты для защиты от зависших операций
- Используйте отдельные пулы потоков с параметрами, соответствующими характеру операций
- Применяйте механизм повторных попыток (retry) для нестабильных внешних сервисов
- Внедряйте схемы предотвращения перегрузки (backpressure) при работе с большими объемами данных
- Тщательно обрабатывайте исключения и завершайте контекст даже при ошибках
- Избегайте блокирующих операций в потоках Executor'а, используйте асинхронные клиенты
Оптимизация производительности Java-приложений с AsyncContext
AsyncContext представляет собой центральный компонент асинхронной обработки в Java Servlet API. Его умелое использование позволяет значительно повысить производительность и масштабируемость веб-приложений. Рассмотрим продвинутые методики и оптимизации, позволяющие раскрыть полный потенциал асинхронных сервлетов. 🚀
AsyncContext предоставляет следующие ключевые функции:
- Управление жизненным циклом асинхронного запроса
- Доступ к объектам запроса и ответа
- Перенаправление запросов между сервлетами
- Установка таймаутов и обработка событий
- Интеграция с слушателями асинхронных событий
Рассмотрим расширенные техники оптимизации:
1. Тонкая настройка пулов потоков
Вместо использования стандартного ExecutorService лучше создать специализированные пулы потоков с параметрами, соответствующими типу операций:
// Для CPU-интенсивных операций (число потоков равно количеству ядер)
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService computeExecutor = new ThreadPoolExecutor(
cpuCores, cpuCores,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactory() {
final AtomicInteger counter = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("compute-worker-" + counter.incrementAndGet());
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
// Для I/O-интенсивных операций (число потоков больше, т.к. операции в основном ждут)
ExecutorService ioExecutor = new ThreadPoolExecutor(
cpuCores * 2, cpuCores * 10,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadFactory() {
final AtomicInteger counter = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("io-worker-" + counter.incrementAndGet());
thread.setDaemon(true);
return thread;
}
},
new ThreadPoolExecutor.AbortPolicy()
);
2. Повторное использование AsyncContext
Вместо создания нового AsyncContext для каждого этапа обработки, можно переиспользовать существующий контекст:
@WebServlet(urlPatterns = "/async-pipeline", asyncSupported = true)
public class PipelineServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Начинаем асинхронную обработку
final AsyncContext asyncContext = request.startAsync();
// Запускаем первую стадию обработки
stage1(asyncContext);
}
private void stage1(AsyncContext context) {
ioExecutor.execute(() -> {
try {
// Получение данных из БД
String data = fetchFromDatabase();
// Сохраняем результат в атрибутах для следующего этапа
context.getRequest().setAttribute("data", data);
// Переходим к следующему этапу в другом пуле потоков
stage2(context);
} catch (Exception e) {
handleError(context, e);
}
});
}
private void stage2(AsyncContext context) {
computeExecutor.execute(() -> {
try {
// Получаем данные из предыдущего этапа
String data = (String) context.getRequest().getAttribute("data");
// Обрабатываем данные (CPU-интенсивная операция)
String processedData = processData(data);
// Переходим к финальному этапу
complete(context, processedData);
} catch (Exception e) {
handleError(context, e);
}
});
}
private void complete(AsyncContext context, String result) {
try {
HttpServletResponse response = (HttpServletResponse) context.getResponse();
response.setContentType("application/json");
response.getWriter().write("{\"result\":\"" + result + "\"}");
} catch (IOException e) {
log("Error writing response", e);
} finally {
context.complete();
}
}
private void handleError(AsyncContext context, Exception e) {
try {
log("Error in async processing", e);
HttpServletResponse response = (HttpServletResponse) context.getResponse();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("Error: " + e.getMessage());
} catch (IOException ioe) {
log("Failed to write error response", ioe);
} finally {
context.complete();
}
}
// Методы имитации операций опущены для краткости
}
3. Использование AsyncContext.dispatch() для делегирования обработки
Метод dispatch() позволяет передавать управление другим сервлетам или JSP-страницам для завершения обработки:
@WebServlet(urlPatterns = "/async-dispatch", asyncSupported = true)
public class DispatchingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync();
executorService.execute(() -> {
try {
// Выполняем асинхронную операцию
Map<String, Object> result = performComplexOperation();
// Сохраняем результат в атрибутах запроса
request.setAttribute("data", result);
// Перенаправляем запрос на JSP для отображения результатов
asyncContext.dispatch("/WEB-INF/views/result.jsp");
} catch (Exception e) {
log("Error in async processing", e);
asyncContext.complete();
}
});
}
}
4. Тайм-ауты и управление ресурсами
Грамотное управление таймаутами и ресурсами критически важно для стабильности системы:
@WebServlet(urlPatterns = "/resource-management", asyncSupported = true)
public class ResourceManagementServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync();
// Устанавливаем разумный таймаут
asyncContext.setTimeout(30000);
// Создаем ресурсы, требующие очистки
final Connection dbConnection = getDatabaseConnection();
// Добавляем обработчик событий для гарантированной очистки ресурсов
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) {
closeResources(dbConnection);
}
@Override
public void onTimeout(AsyncEvent event) {
log("Request timed out");
handleTimeout((HttpServletResponse) event.getSuppliedResponse());
closeResources(dbConnection);
}
@Override
public void onError(AsyncEvent event) {
log("Error occurred", event.getThrowable());
closeResources(dbConnection);
}
@Override
public void onStartAsync(AsyncEvent event) {
// Обычно не требует специальной обработки
}
private void closeResources(Connection conn) {
try {
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
log("Error closing database connection", e);
}
}
private void handleTimeout(HttpServletResponse response) {
try {
response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
response.getWriter().write("Request processing timed out");
} catch (IOException e) {
log("Failed to write timeout response", e);
}
}
});
// Продолжаем асинхронную обработку
executorService.execute(() -> processRequestWithConnection(asyncContext, dbConnection));
}
// Другие методы опущены для краткости
}
5. Мониторинг и метрики
Для эффективной оптимизации необходимо собирать метрики производительности:
@WebServlet(urlPatterns = "/monitored-async", asyncSupported = true)
public class MonitoredAsyncServlet extends HttpServlet {
private final MetricsRegistry metricsRegistry = new MetricsRegistry();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Начинаем отсчёт времени
long startTime = System.currentTimeMillis();
String operationType = request.getParameter("operation");
final AsyncContext asyncContext = request.startAsync();
// Увеличиваем счётчик активных запросов
metricsRegistry.incrementActiveRequests();
executorService.execute(() -> {
try {
// Выполняем операцию
String result = performOperation(operationType);
// Записываем ответ
HttpServletResponse asyncResponse =
(HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("text/plain");
asyncResponse.getWriter().write(result);
} catch (Exception e) {
log("Error in async processing", e);
metricsRegistry.incrementErrorCount(operationType);
} finally {
// Обновляем метрики
long processingTime = System.currentTimeMillis() – startTime;
metricsRegistry.recordOperationTime(operationType, processingTime);
metricsRegistry.decrementActiveRequests();
asyn