Virtual Threads не ускоряют то, что упирается в блокировку
Вопрос звучит просто: когда брать виртуальные потоки вместо платформенных. Ловушка в том, что кандидаты отвечают «всегда, они дешёвые» — и это неверно в половине случаев.
Virtual thread (Java 21+) — это поток, которым управляет JVM, а не ОС. Создать миллион виртуальных потоков реально дёшево: каждый занимает несколько сотен байт, а не мегабайт стека, как платформенный. Когда виртуальный поток блокируется на I/O — сокет, HTTP-клиент, JDBC-драйвер с поддержкой — JVM снимает его с несущего платформенного потока (carrier thread) и освобождает carrier для другого виртуального потока. Это и даёт выигрыш: тысячи параллельных запросов на горстке OS-потоков.
Но есть pinning — ситуация, когда виртуальный поток не отцепляется от carrier на время блокировки. Классический случай — synchronized блок: пока виртуальный поток внутри synchronized ждёт I/O, он держит carrier thread мёртвой хваткой. Второй случай — нативные методы и некоторые JNI-вызовы.
synchronized (lock) { // блокирующий JDBC-вызов внутри // виртуальный поток запинен на carrier thread resultSet = statement.executeQuery(sql); }
Если таких синхронизированных блоков с блокирующими вызовами много, а carrier threads (их по умолчанию столько, сколько ядер) заняты запиненными потоками — пропускная способность не растёт, а может даже упасть относительно обычного пула потоков.
Спросят следом: как обнаружить pinning в проде? Ответ — JFR-событие jdk.VirtualThreadPinned, либо флаг -Djdk.tracePinnedThreads=full в логах. И второй вопрос: если узкое место — это пул соединений к БД на 20 коннектов, а не количество потоков, поможет ли переход на виртуальные потоки? Нет — они не создают новые соединения из воздуха, throughput упрётся в тот же пул.
Viral threads решают проблему дорогих OS-потоков под блокирующий I/O. Они не решают проблему медленной внешней системы и не отменяют synchronized как источник contention.
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки