Классическая проблема серверного кода: поток, ожидающий ответа от базы или соседнего сервиса, занят — он ничего не делает, но держит мегабайты стека. Тысячи одновременных запросов — тысячи занятых потоков. Kotlin отвечает корутинами, Java 21 — virtual threads. Идея одна, механика разная — разберём обе и сравним честно.

suspend: функция, которая умеет ждать бесплатно

suspend fun loadUser(id: String): User =
    httpClient.get("/users/$id").body()

suspend fun loadProfile(id: String): Profile {
    val user = loadUser(id)               // «ожидание» без блокировки потока
    val orders = loadOrders(user.id)
    return Profile(user, orders)
}

Модификатор suspend означает: функция может приостановиться, отпустив поток, и продолжиться позже — возможно, на другом потоке. Код при этом читается как обычный последовательный — никаких колбэков и цепочек thenCompose, как с CompletableFuture. Приостановка происходит в точках вызова других suspend-функций; правило вежливости то же, что у потоков: внутри suspend-функции нельзя звать блокирующие API без обёртки.

Тысячи корутин живут на пуле из нескольких потоков: приостановилась одна — поток тут же исполняет другую.

Structured concurrency: у задач есть родитель

suspend fun loadDashboard(id: String): Dashboard = coroutineScope {
    val user = async { loadUser(id) }
    val stats = async { loadStats(id) }
    Dashboard(user.await(), stats.await())   // параллельно, но в рамках scope
}

coroutineScope не завершится, пока не завершатся все запущенные внутри корутины. Упала одна — отменяются остальные, ошибка поднимается наверх. Это ответ на вечную проблему «потерянных» фоновых задач: у каждой корутины есть родитель, и утечь незамеченной она не может. Отмена — встроенная: отменился родитель (клиент закрыл соединение) — отменилось всё дерево.

Диспетчеры задают, где исполняться: Dispatchers.IO — для блокирующего ввода-вывода, Dispatchers.Default — для вычислений. Смена — одной функцией withContext(Dispatchers.IO) { ... }.

Virtual threads: тот же выигрыш без нового синтаксиса

Java 21 решает ту же проблему иначе: virtual threads — потоки, которыми управляет JVM, а не операционная система. Блокирующий вызов на виртуальном потоке не держит системный поток — JVM «отстёгивает» его сама. Код остаётся обычным блокирующим, suspend-модификаторы не нужны.

КорутиныVirtual threads
Как достигается дешёвое ожиданиекомпилятор переписывает suspend-функцииJVM паркует виртуальный поток при блокировке
Изменения в кодеsuspend в сигнатурах, scope, диспетчерыпочти нет: старый блокирующий код
Structured concurrencyвстроена и зрелаяпоявляется в JDK, пока моложе
Отмена задачвстроена в модельпрерывание потоков, как раньше
Где живётKotlin (и Android)любой JVM-язык с Java 21+

Честный вывод: если цель — только «не держать потоки на ожидании» в Spring-приложении, virtual threads дают это почти бесплатно. Корутины выигрывают там, где нужна модель целиком: structured concurrency, отмена, потоки данных (Flow), Android. В Kotlin-проекте на Spring Boot оба пути рабочие — и они не исключают друг друга.

Практические правила

  • suspend заражает сигнатуры вверх по стеку — как checked exceptions: звать suspend можно только из suspend или из scope. Планируйте границу «синхронный мир ↔ корутины» осознанно.
  • Не смешивайте блокирующие вызовы (JDBC, старые клиенты) с Dispatchers.Default — заворачивайте в Dispatchers.IO.
  • GlobalScope — почти всегда ошибка: корутина без родителя, та самая утечка, против которой придумана structured concurrency.
  • Основы конкурентности никуда не делись: гонки, видимость изменений, неизменяемость — фундамент тот же.

Коротко

  • Корутины — дешёвые единицы конкурентности: suspend-функция приостанавливается, отпуская поток; тысячи корутин живут на маленьком пуле.
  • Structured concurrency: у корутины всегда есть родитель, scope ждёт детей, ошибки и отмена распространяются по дереву — фоновые задачи не теряются.
  • Virtual threads из Java 21 решают ту же проблему без изменения кода — JVM паркует виртуальный поток при блокирующем вызове.
  • Выбор: только убрать «дорогие ожидания» — хватит virtual threads; нужна модель с отменой, scope и Flow — корутины.
  • Блокирующие вызовы — в Dispatchers.IO, GlobalScope — не использовать.

Что почитать дальше

  • Kotlin и Spring Boot — последний шаг маршрута.
  • Многопоточность со Spring на практике — пулы, @Async и virtual threads в Java-проекте.
  • Многопоточность и конкурентность — фундамент: память, гонки, синхронизация.