Проблема, которую решают
Платформенные потоки дороги, поэтому их число ограничено пулом. В модели thread-per-request на блокирующем IO это упирается в потолок: тысячи одновременных запросов требуют тысяч потоков, а столько ОС-потоков не потянуть.
Альтернатива раньше была одна — асинхронный/реактивный код, который сложнее читать и отлаживать. Виртуальные потоки (Java 21) возвращают простой блокирующий стиль, снимая ограничение на количество.
Как устроены
Виртуальный поток — это лёгкий поток, управляемый JVM, а не ОС. Их могут быть миллионы. Для выполнения виртуальный поток монтируется на платформенный поток-носитель (carrier); когда он встречает блокирующую операцию (IO, ожидание), JVM размонтирует его, освобождая carrier для других виртуальных потоков, а стек сохраняет в куче.
Так блокирующий вызов больше не держит дорогой ОС-поток — он лишь паркует дешёвый виртуальный.
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
for (var task : tasks) exec.submit(task); // поток на задачу — это норм
}

Когда применять
Виртуальные потоки — про высокую конкуренцию на блокирующем IO (вызовы БД, HTTP, очередей): thread-per-request снова масштабируется до десятков/сотен тысяч одновременных операций.
Они НЕ ускоряют CPU-bound работу: вычисления всё равно упираются в число ядер, и тут по-прежнему уместны платформенные потоки/ForkJoin.
Не пулить и не блокировать carrier
Виртуальные потоки не пулят: они дёшевы, поэтому создают по одному на задачу (newVirtualThreadPerTaskExecutor). Пул виртуальных потоков бессмыслен и вреден.
Главный подводный камень — pinning: виртуальный поток, который не может размонтироваться, «прибивает» carrier. В Java 21–23 это происходило при блокировке внутри synchronized-блока, поэтому для длинных критических секций советовали ReentrantLock. В JDK 24 (JEP 491) synchronized больше не прибивает поток, и совет актуален только для старых версий. Остаётся закрепление на нативных вызовах (JNI) — его находят по событию JFR jdk.VirtualThreadPinned. И не кладут тяжёлые объекты в ThreadLocal: виртуальных потоков миллионы.
Как отвечать на собеседовании
Объясните выгоду: блокирующий стиль кода + масштаб реактивного за счёт монтирования/размонтирования на carrier при блокировке. Подчеркните — для IO-bound, не для CPU-bound; не пулить (поток на задачу); и знайте про pinning на synchronized/нативных вызовах (лечится ReentrantLock). Бонус — упомяните structured concurrency (StructuredTaskScope) для управления группой подзадач.