В реальном коде потоки руками не создают и wait/notify не пишут. На собесе ждут, что вы знаете пулы, CompletableFuture, ConcurrentHashMap и в 2026 году виртуальные потоки Java 21.
ExecutorService и пулы
try (ExecutorService pool = Executors.newFixedThreadPool(8)) { // close() с Java 19
Future<Integer> f = pool.submit(() -> compute());
Integer r = f.get(2, TimeUnit.SECONDS);
}
Под капотом ThreadPoolExecutor(core, max, keepAlive, queue, threadFactory, rejectionHandler). Логика при новой задаче: потоков меньше core, создать поток; иначе положить в очередь; очередь полна, создать поток до max; иначе сработает политика отказа (AbortPolicy бросает RejectedExecutionException, CallerRunsPolicy выполняет в вызывающем потоке).
Ловушки фабрик Executors:
newFixedThreadPoolиспользует неограниченнуюLinkedBlockingQueue: при перегрузке очередь растёт до OOM, аmaxне работает.newCachedThreadPoolсоздаёт потоки без ограничения.
В проде пул создают явно, с ограниченной очередью, именами потоков и метриками. Пул обязательно закрывают (shutdown, awaitTermination или try-with-resources).
Исключение в задаче submit не печатается: оно сохраняется в Future и вылетит из get() в виде ExecutionException.
CompletableFuture
CompletableFuture<Order> order = CompletableFuture.supplyAsync(() -> loadOrder(id), ioPool);
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> loadUser(uid), ioPool);
order.thenCombine(user, OrderView::new)
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(e -> OrderView.empty())
.thenAccept(this::send);
thenApplyпреобразует результат,thenComposeподставляет следующий асинхронный шаг (аналогflatMap),thenCombineобъединяет два независимых.allOfждёт все,anyOfпервый.exceptionallyиhandleобрабатывают ошибки,orTimeoutиcompleteOnTimeoutограничивают время.- Без явного executor задачи
*Asyncидут вForkJoinPool.commonPool(). Блокирующий I/O туда класть нельзя.
Конкурентные коллекции
ConcurrentHashMap: чтение без блокировок, запись через CAS в пустую корзину или блокировку первого узла корзины (с Java 8, сегментов больше нет).nullзапрещён. Атомарные составные операции:putIfAbsent,computeIfAbsent,merge. Итераторы weakly consistent, не бросают CME.CopyOnWriteArrayList: каждая запись копирует массив. Для редко меняемых списков с частым чтением (слушатели).BlockingQueue(ArrayBlockingQueue,LinkedBlockingQueue):putждёт места,takeждёт элемент. Основа producer-consumer.
Ошибка: if (!map.containsKey(k)) map.put(k, v) даже на ConcurrentHashMap это гонка check-then-act. Правильно map.computeIfAbsent(k, key -> v).
Синхронизаторы и блокировки
ReentrantLock: какsynchronized, плюсtryLockс таймаутом, прерываемое ожидание, несколькоCondition.unlock()всегда вfinally.ReadWriteLock,StampedLock: много читателей или один писатель.CountDownLatch: дождаться N событий, одноразовый.CyclicBarrier: N потоков ждут друг друга, многоразовый.Semaphore: не больше N одновременных доступов.- Атомики:
AtomicInteger,AtomicReferenceна CAS;LongAdderдля счётчиков под высокой конкуренцией.
Виртуальные потоки (Java 21)
Платформенный поток это поток ОС: дорогой (мегабайт стека, переключение через ядро), их тысячи. Виртуальный поток управляется JVM: дешёвый, их можно миллионы. Когда виртуальный поток блокируется на I/O, JVM снимает его с потока-носителя (carrier), и носитель берёт другой.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> callHttp()); // поток на задачу
}
}
Thread.ofVirtual().start(() -> work());
Что важно знать:
- Ускоряют пропускную способность при блокирующем I/O, а не вычисления. CPU-задачам они не помогают.
- Не пулят: создают новый на каждую задачу. Ограничение параллелизма делают через
Semaphore, а не размером пула. - Pinning: в Java 21-23 блокировка внутри
synchronizedзакрепляет виртуальный поток за носителем, и носители кончаются. Совет был заменятьsynchronizedнаReentrantLock. В Java 24 это исправлено (JEP 491), в Java 21 актуально. ThreadLocalработает, но при миллионе потоков это миллион копий. Для передачи контекста естьScopedValue(стабилен с Java 25).- В Spring Boot 3.2+ включаются свойством
spring.threads.virtual.enabled=true.
Что спросят на собесе
- Как работает ThreadPoolExecutor, что будет при переполнении очереди?
- Чем опасен
Executors.newFixedThreadPoolв проде? - Как устроен ConcurrentHashMap в Java 8+? Почему он не принимает null?
thenApplyпротивthenCompose, куда уходят задачи без executor?- Что такое виртуальные потоки, когда они помогают и что такое pinning?