Должны ли контроллеры быть потокобезопасными или это обеспечивает фреймворк
Что проверяют
Знаете ли вы, что контроллер в Spring это синглтон, и понимаете ли, где заканчиваются гарантии фреймворка. Следом обычно показывают контроллер с изменяемым полем и просят найти проблему или спрашивают, что меняется с виртуальными потоками.
Как отвечать
Сначала факт: @RestController и @Controller это обычные бины со scope singleton. Экземпляр один на контекст, а запросы идут параллельно: во встроенном Tomcat по умолчанию до 200 потоков (server.tomcat.threads.max).
Что фреймворк гарантирует:
- изоляцию данных запроса: аргументы метода,
@RequestBody,@PathVariableи локальные переменные живут в стеке своего потока; - прокси для запросных объектов: внедрённый
HttpServletRequestили бин со@RequestScopeна деле прокси, который в каждом потоке указывает на свой запрос; - корректную публикацию бинов: зависимости, внедрённые при старте контекста, видны всем потокам запросов.
Чего фреймворк не гарантирует, так это безопасность вашего изменяемого состояния. Отсюда правила:
- контроллер без состояния, зависимости через конструктор в
final-поля; - сервисы, которые он вызывает, тоже синглтоны и тоже обязаны быть потокобезопасными;
- если общее состояние действительно нужно, берите
AtomicLong,LongAdder,ConcurrentHashMap, а неsynchronizedна методе, который выстроит все запросы в очередь; - не держите в полях непотокобезопасные утилиты:
SimpleDateFormatменяется наDateTimeFormatter.
С виртуальными потоками (Java 21 и spring.threads.virtual.enabled=true в Spring Boot 3.2+) картина та же: потоков больше, контроллер по-прежнему один.
Где спотыкаются
Говорят, что Spring создаёт контроллер на каждый запрос. По умолчанию это неверно, и дальше интервью уходит в разбор ошибки.
Сохраняют в поле данные текущего пользователя, например private User currentUser. Под нагрузкой один клиент увидит чужие данные, а в локальной проверке с одним запросом всё работает.
Внедряют prototype-бин в контроллер-синглтон, рассчитывая на свежий экземпляр в каждом запросе. Он создаётся один раз при внедрении и дальше общий для всех; нужен ObjectProvider или бин со @RequestScope.