Как работают сервлеты: создание, сессии, общие переменные и многопоточность
Основы работы сервлетов: жизненный цикл и создание
Сервлет — это не CGI-скрипт, который запускается заново для каждого запроса. В Java EE сервлеты работают в контейнере сервлетов (например, Tomcat, Jetty). Контейнер управляет их жизненным циклом:
- Загрузка класса: при старте приложения или при первом запросе (зависит от настройки
load-on-startup) - Создание экземпляра: вызывается конструктор (один раз)
- Инициализация: вызывается метод
init()(один раз) - Обработка запросов: для каждого запроса вызывается
service()(doGet/doPost) в отдельном потоке - Уничтожение: вызывается
destroy()при остановке приложения
Ключевой момент: один экземпляр сервлета обслуживает все запросы. Именно поэтому ваш счетчик ведет себя странно — он общий для всех пользователей.
Почему счетчик "скачет"?
Вы столкнулись с проблемой многопоточного доступа. Когда два пользователя одновременно вызывают doGet(), оба потока выполняют counter++ — операцию, которая не атомарна (она состоит из чтения, увеличения и записи). Результат: один инкремент может "потеряться".
Пример проблемы:
// Поток 1: читает counter = 5
// Поток 2: читает counter = 5
// Поток 1: записывает counter = 6
// Поток 2: записывает counter = 6 (потеряли одно посещение!)
Правильное решение для подсчета посещений
Есть два подхода:
1. Синхронизация (для общего счетчика):
@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
private int counter = 0;
@Override
protected synchronized void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
counter++;
resp.getWriter().println("Total visits: " + counter);
}
}
Но это плохое решение — синхронизация замедлит все запросы, и счетчик всё равно будет общим.
2. Использование сессий (для персонального счетчика):
@WebServlet("/mycounter")
public class UserCounterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession();
Integer counter = (Integer) session.getAttribute("counter");
if (counter == null) {
counter = 0;
}
counter++;
session.setAttribute("counter", counter);
resp.getWriter().println("Your visits: " + counter);
}
}
Как работают сессии?
- Сессия создается при первом вызове
req.getSession() - Контейнер генерирует уникальный JSESSIONID и отправляет его в cookie браузеру
- При каждом следующем запросе браузер присылает этот ID обратно
- Данные сессии хранятся на сервере (в памяти или БД) и уникальны для каждого пользователя
Общие переменные (instance variables)
Переменные экземпляра сервлета общие для всех потоков. Это как глобальные переменные. Если нужно хранить данные, специфичные для запроса — используйте параметры запроса или атрибуты запроса:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String userName = req.getParameter("name"); // данные запроса
req.setAttribute("calculated", someValue); // для JSP/вью
}
Многопоточность в сервлетах
Контейнер использует пул потоков. По умолчанию (в Tomcat) ~200 потоков. Каждый запрос обрабатывается в отдельном потоке, но все они используют один экземпляр сервлета. Поэтому:
- Локальные переменные в методах — безопасны (у каждого потока свой стек)
- Переменные экземпляра — требуют синхронизации или должны быть thread-safe
- Статические переменные — тоже требуют синхронизации
- Лучше использовать
AtomicIntegerилиConcurrentHashMapдля потокобезопасности
Итог: ваш сервлет создается один раз. Используйте сессии для данных пользователя, атрибуты запроса для данных одного запроса, и никогда не храните изменяемое состояние в переменных экземпляра без синхронизации.
<<Дополнение: альтернативный взгляд на архитектуру сервлетов
В дополнение к основному ответу хочу обратить внимание на современные практики. Проблема, с которой вы столкнулись — классический пример того, почему в Java EE (Jakarta EE) появились более высокоуровневые абстракции.
Использование фильтров и слушателей
Если вам нужно считать общее количество посещений всего приложения (не сервлета), лучше использовать ServletContextListener:
@WebListener
public class AppListener implements ServletContextListener {
private AtomicInteger totalVisits = new AtomicInteger(0);
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("totalVisits", totalVisits);
}
}
Затем в любом сервлете: ((AtomicInteger)getServletContext().getAttribute("totalVisits")).incrementAndGet().
Почему не стоит использовать сервлеты напрямую?
В современных приложениях сервлеты обычно служат "контроллерами" в MVC, а всю логику выносят в отдельные классы (сервисы). Например, популярный Spring MVC скрывает работу с сервлетами за аннотациями @Controller и @RequestMapping. Там те же принципы: бины по умолчанию — синглтоны (один экземпляр), а сессии и запросы управляются через специальные скоупы.
Практический совет
Если вы пишете на чистом Java EE без фреймворков, всегда спрашивайте себя:
- Эти данные принадлежат одному пользователю? → сессия или запрос
- Эти данные общие для всех? →
ServletContextс синхронизацией - Эти данные временные для одного действия? → локальные переменные метода
И никогда не используйте переменные экземпляра сервлета для хранения состояния — это прямой путь к трудноотлавливаемым багам в многопоточной среде.