← Назад к вопросам

В разных или одном потоке запросы идут в контроллер

2.0 Middle🔥 121 комментариев
#Spring Framework#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

В каких потоках выполняются запросы в контроллер?

Отличный вопрос, касающийся многопоточности в веб-приложениях. Ответ: в разных потоках, и это работает так.

Суть ответа

Каждый HTTP запрос к контроллеру обрабатывается отдельным потоком из пула потоков (thread pool). Это ключевая особенность всех популярных веб-фреймворков Java.

// Пример: контроллер Spring Boot
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // Этот метод может быть вызван из РАЗНЫХ потоков
        // для разных запросов одновременно
        System.out.println("Текущий поток: " + Thread.currentThread().getName());
        return userService.findById(id);
    }
}

// Вывод типичного Spring Boot приложения:
// GET /api/users/1 -> поток: http-nio-8080-exec-1
// GET /api/users/2 -> поток: http-nio-8080-exec-2
// GET /api/users/3 -> поток: http-nio-8080-exec-3

Архитектура веб-сервера (Tomcat/Jetty/Undertow)

Вот как организована многопоточность:

Клиент 1 -> запрос 1 ---|                    |--- Поток 1
Клиент 2 -> запрос 2 ----|  Пул потоков     |--- Поток 2
Клиент 3 -> запрос 3 ----| (200 потоков)    |--- Поток 3
Клиент N -> запрос N ----|                    |--- Поток N

Вот схема в коде:

// Симуляция работы web сервера
public class SimpleWebServer {
    private ExecutorService threadPool = Executors.newFixedThreadPool(200);
    
    public void handleIncomingRequest(HttpRequest request) {
        // Каждый запрос обрабатывается в отдельном потоке
        threadPool.submit(() -> {
            Controller controller = new UserController();
            controller.handleRequest(request);
        });
    }
}

В Spring Boot конкретно

Вот конфигурация по умолчанию:

# application.properties
# Максимальное количество рабочих потоков в Tomcat
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=10

# Это означает:
# - До 10 потоков всегда готовы
# - До 200 потоков максимум
# - Дополнительные запросы встают в очередь
// Проверка текущего потока
@GetMapping("/thread-info")
public Map<String, String> getThreadInfo() {
    Map<String, String> info = new HashMap<>();
    Thread current = Thread.currentThread();
    
    info.put("thread_name", current.getName());
    info.put("thread_id", String.valueOf(current.getId()));
    info.put("thread_state", current.getState().toString());
    
    return info;
}

// Результат:
// {
//   "thread_name": "http-nio-8080-exec-15",
//   "thread_id": "42",
//   "thread_state": "RUNNABLE"
// }

Важные следствия многопоточности

1. Thread-safe переменные нужны для общих ресурсов

// ❌ Опасно: если несколько потоков изменяют это одновременно
public class Counter {
    private int count = 0; // НЕ thread-safe!
    
    public void increment() {
        count++; // Race condition!
    }
}

// ✅ Правильно: используй synchronized или AtomicInteger
public class SafeCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet(); // Atomic операция
    }
}

2. Thread-local переменные для изоляции

// Каждый поток имеет свою копию
public class SecurityContext {
    private static ThreadLocal<User> userContext = new ThreadLocal<>();
    
    public static void setCurrentUser(User user) {
        userContext.set(user); // Видно только в текущем потоке
    }
    
    public static User getCurrentUser() {
        return userContext.get();
    }
}

// В контроллере:
@GetMapping("/me")
public User getCurrentUser() {
    // Каждый запрос видит СВОЮ копию пользователя
    return SecurityContext.getCurrentUser();
}

3. Синхронизация для критических секций

// Если нужно общее состояние
public class Database {
    private Map<String, Object> cache = new ConcurrentHashMap<>();
    // ConcurrentHashMap автоматически синхронизирует доступ
    
    public void put(String key, Object value) {
        cache.put(key, value); // Безопасно из разных потоков
    }
}

Блокировка потоков

Что происходит, если все потоки заняты?

@GetMapping("/slow")
public String slowEndpoint() throws InterruptedException {
    Thread.sleep(10000); // 10 секунд обработки
    return "Done";
}

// Если максимум 200 потоков и 200 запросов идут на /slow,
// то 201-й запрос встанет в очередь и будет ждать.

Распределённые запросы

В clustered сценарии (несколько серверов):

То же самое, но на разных машинах:

Сервер 1 (8 ядер)  -> 200 потоков
Сервер 2 (8 ядер)  -> 200 потоков
Сервер N (8 ядер)  -> 200 потоков

Лучшие практики

  1. Не создавай потоки вручную в контроллерах — используй разделяемый пул
  2. Избегай блокировки потоков в обработчиках (долгие операции → async)
  3. Используй concurrent коллекции для разделяемых данных
  4. Thread-safe сервисы — убедись, что бизнес-логика потокобезопасна

Асинхронная обработка (для улучшения)

// Если операция долгая, используй async
@GetMapping("/async-data")
public CompletableFuture<String> getDataAsync() {
    return CompletableFuture.supplyAsync(() -> {
        // Выполняется в отдельном потоке (не из основного пула)
        return "Данные загружены";
    });
}

Итог

Каждый HTTP запрос обрабатывается отдельным потоком из пула. Это позволяет серверу обрабатывать множество одновременных запросов. Но это требует внимания к thread-safety и избежания блокировок потоков.