Как работает многопоточная обработка в Servlet контейнере
Что проверяют
Понимаете ли вы, что экземпляр сервлета один на всех, а потоков много, и что из этого следует для полей, сессии и ThreadLocal. Следом обычно спрашивают про размер пула в Tomcat, асинхронные сервлеты и виртуальные потоки.
Как отвечать
Контейнер создаёт один экземпляр на каждое объявление сервлета, вызывает init() один раз, а дальше каждый запрос обслуживает поток из пула, вызывая на этом же экземпляре service() и doGet() или doPost(). Модель thread-per-request: поток занят запросом от начала до конца. У Tomcat в NIO-коннекторе отдельные потоки принимают соединения и следят за сокетами, а рабочий пул по умолчанию ограничен maxThreads="200". Когда все заняты, запросы ждут в очереди.
Что из этого следует:
- поля сервлета: общие для всех потоков. Счётчик делайте через
AtomicLongилиLongAdder, конфигурацию читайте вinit()и не меняйте; - локальные переменные,
requestиresponse: принадлежат одному запросу и безопасны, пока не утекли в другой поток; HttpSession: не потокобезопасна. Два параллельных запроса одного пользователя из вкладок или AJAX работают с ней одновременно;ThreadLocal: поток из пула переживает запрос, поэтому MDC, контекст безопасности и прочее нужно чистить вfinally, иначе данные уедут следующему пользователю.
Для долгих операций есть request.startAsync() из Servlet 3.0: поток контейнера возвращается в пул, а ответ дописывается позже через AsyncContext. С Java 21 Tomcat умеет обслуживать запросы на виртуальных потоках, в Spring Boot это включается свойством spring.threads.virtual.enabled=true. Контроллеры Spring тоже синглтоны. И пакет в актуальных версиях jakarta.servlet, а не javax.servlet.
Где спотыкаются
Думают, что на каждый запрос создаётся новый экземпляр сервлета, и хранят данные запроса в полях.
Советуют «хранить состояние в сессии, она изолирована». Она изолирована между пользователями, но не между запросами одного пользователя.
Синхронизируют весь doGet() или делают synchronized (this) на запись, а читают поле без блокировки. Первое превращает пул в очередь из одного потока, второе оставляет гонку видимости.