Avatar ·

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

Всем привет! Я только начал разбираться с Java EE и сервлетами, и у меня полная каша в голове. Написал простой сервлет для подсчета посещений страницы, но он ведет себя странно. Вот мой код: ```java @WebServlet("/counter") public class CounterServlet extends HttpServlet { private int counter = 0; // переменная экземпляра @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { counter++; resp.getWriter().println("Visits: " + counter); } } ``` Когда я открываю страницу в двух разных вкладках браузера, счетчик почему-то увеличивается сразу на 2 или даже больше. Я думал, что для каждого пользователя создается свой экземпляр сервлета, но, похоже, это не так. Еще я не понимаю: 1. Когда именно создается сервлет? Один раз при старте или при каждом запросе? 2. Как работают сессии? Я хочу считать посещения для каждого пользователя отдельно. 3. Если сервлет один на всех, то как быть с общими данными? Получается, что переменная `counter` доступна всем пользователям одновременно? 4. Как многопоточность влияет на сервлеты? Может ли два пользователя одновременно изменить одну и ту же переменную? Объясните, пожалуйста, как это все работает на самом деле. А то в учебниках написано сложно, а на практике я ничего не понимаю. Спасибо! <<>>

Основы работы сервлетов: жизненный цикл и создание

Сервлет — это не 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 с синхронизацией
  • Эти данные временные для одного действия? → локальные переменные метода

И никогда не используйте переменные экземпляра сервлета для хранения состояния — это прямой путь к трудноотлавливаемым багам в многопоточной среде.