Как работают сервлеты: создание, сессии, общие переменные и многопоточность
Как работают сервлеты: полный разбор
Отличные вопросы! Давайте разберём всё по порядку. Ваша проблема — классическая ошибка начинающих, связанная с непониманием жизненного цикла сервлетов и областей видимости переменных.
1. Жизненный цикл сервлета (Instantiation)
Сервлет создаётся один раз при первом обращении к нему (или при старте сервера, если настроена загрузка). Контейнер сервлетов (Tomcat, Jetty и т.д.) вызывает метод init() один раз, а затем для каждого запроса — метод service(), который делегирует работу doGet() или doPost().
Важно: Экземпляр сервлета один на всех пользователей. Он живёт в памяти, пока сервер работает, и обслуживает все входящие запросы.
2. Общие переменные и многопоточность
Поскольку сервлет один, все его поля (как ваш counter) являются общими для всех потоков. Когда два пользователя одновременно вызывают doGet(), они работают с одной и той же переменной counter. Это приводит к:
- Гонке данных (race condition): операция
counter++не атомарна (чтение → инкремент → запись). Два потока могут прочитать одно и то же значение и оба записать +1, хотя должно быть +2. - Неопределённому поведению: вы видите, что счётчик суммируется для всех пользователей.
Как исправить ваш пример? Если вам нужен глобальный счётчик, используйте атомарные типы:
import java.util.concurrent.atomic.AtomicInteger;
public class CounterServlet extends HttpServlet {
private AtomicInteger counter = new AtomicInteger(0);
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int current = counter.incrementAndGet();
response.getWriter().println("Всего посещений: " + current);
}
}
Но чаще всего вам не нужен общий счётчик. Для подсчёта действий конкретного пользователя используйте сессии.
3. Сессии (Sessions) — данные для каждого пользователя
Чтобы хранить данные отдельно для каждого пользователя (корзина, логин, настройки), используйте HttpSession. Сессия создаётся автоматически при первом обращении к request.getSession() и идентифицируется по cookie (JSESSIONID) или перезаписи URL.
Пример счётчика для каждого пользователя:
public class UserCounterServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession();
Integer counter = (Integer) session.getAttribute("counter");
if (counter == null) {
counter = 0;
}
counter++;
session.setAttribute("counter", counter);
response.getWriter().println("Вы посетили эту страницу " + counter + " раз(а)");
}
}
Теперь каждый браузер (пользователь) будет видеть свой личный счётчик, потому что данные хранятся в сессии, а не в поле сервлета.
4. Правильная архитектура: что где хранить
В сервлетах есть три уровня хранения данных:
- Поля сервлета — общие для всех потоков. Используйте только для read-only данных (конфигурация, Connection Pool).
- Атрибуты запроса (
request.setAttribute()) — живут только в рамках одного HTTP-запроса. Идеально для передачи данных между сервлетом и JSP. - Атрибуты сессии (
session.setAttribute()) — живут, пока пользователь не закроет браузер или не истечёт таймаут. Для данных, которые должны сохраняться между запросами одного пользователя. - ServletContext (
getServletContext().setAttribute()) — глобальные данные для всего приложения (всех пользователей). Используйте с осторожностью, синхронизируя доступ.
5. Безопасность многопоточности в сервлетах
По умолчанию сервлеты не потокобезопасны. Контейнер создаёт один экземпляр и пул потоков для обработки запросов. Правила:
- Не используйте поля экземпляра для изменяемых данных.
- Если нужно общее состояние — синхронизируйте или используйте
synchronized,AtomicInteger,ConcurrentHashMap. - Локальные переменные в методах (
doGet) — потокобезопасны (каждый поток имеет свой стек). - Для сложной логики рассмотрите @SingleThreadModel (устарел) или используйте фреймворки (Spring MVC), которые управляют состоянием грамотно.
Итог: Ваш первоначальный код суммировал посещения всех пользователей, потому что counter — поле экземпляра сервлета, которое разделяется между потоками. Для индивидуальных данных используйте сессии, для общих — атомарные типы с пониманием последствий.
Альтернативный подход: использование ServletContextListener и Dependency Injection
Дополню ответ коллеги. Если вам нужно управлять состоянием на уровне приложения (например, счётчик всех посещений сайта), лучше вынести логику за пределы сервлета — это улучшит тестируемость и разделение ответственности.
Пример с использованием ServletContextListener для инициализации общего счётчика:
@WebListener
public class AppListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("totalCounter", new AtomicInteger(0));
}
}
@WebServlet("/count")
public class CounterServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
AtomicInteger total = (AtomicInteger) getServletContext()
.getAttribute("totalCounter");
int current = total.incrementAndGet();
response.getWriter().println("Всего: " + current);
}
}
Также в современных Java EE приложениях (Jakarta EE) принято использовать CDI (Contexts and Dependency Injection) для управления состоянием. Например, бины с областью видимости @SessionScoped или @ApplicationScoped:
import javax.inject.Singleton;
import java.util.concurrent.atomic.AtomicInteger;
@Singleton
public class VisitCounter {
private AtomicInteger count = new AtomicInteger(0);
public int increment() { return count.incrementAndGet(); }
}
Затем внедряете этот бин в сервлет через @Inject. Это даёт более чистую архитектуру и упрощает переход на Spring Boot в будущем.
Совет: для учебных целей подход с сессиями и атрибутами — норм, но в продакшене старайтесь минимизировать логику в сервлетах, вынося её в сервисы и бины.