Вопрос 14 · Раздел 19

Что такое блокирующий код и как его отличить от неблокирующего

но внутри использовать get()/join(), что делает его блокирующим. И наоборот: метод без Async может возвращать CompletableFuture и быть неблокирующим. 4. Работает с синхронными I/O

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Блокирующий код — это код, который останавливает поток и ждёт завершения операции.

Неблокирующий код — поток не ждёт, а продолжает выполнять другие задачи.

// ❌ Блокирующий — поток ждёт
String result = future.get();  // ждёт пока задача завершится
Thread.sleep(1000);            // ждёт 1 секунду
InputStream.read();            // ждёт данные

// ✅ Неблокирующий — поток свободен
CompletableFuture.supplyAsync(() -> {
    return "result";  // выполнится в другом потоке
}).thenAccept(result -> System.out.println(result));

// Основной поток продолжает работу

Простая аналогия:

  • Блокирующий — стоите в очереди и ждёте
  • Неблокирующий — оставили заявку и ушли, вам перезвонят

🟡 Middle Level

Блокирующие операции

I/O блокирующие:

// ❌ Блокирующие
InputStream.read();           // ждёт данные
OutputStream.write(data);     // ждёт отправки
Socket.accept();              // ждёт соединения
Files.readAllBytes(path);     // ждёт чтения

// ✅ Неблокирующие (NIO)
AsynchronousFileChannel.read(buffer, position, null, completionHandler);
AsynchronousSocketChannel.connect(remote);

Сетевые блокирующие:

// ❌ Блокирующий HTTP вызов
HttpResponse<String> response = HttpClient.newHttpClient()
    .send(request, BodyHandlers.ofString());  // блокирует!

// ✅ Неблокирующий HTTP вызов
HttpClient.newHttpClient()
    .sendAsync(request, BodyHandlers.ofString())  // возвращает CompletableFuture
    .thenAccept(response -> process(response));

Database:

// ❌ Блокирующий
ResultSet rs = statement.executeQuery(sql);  // блокирует

// ✅ Неблокирующий (R2DBC)
Mono<Result> result = client.sql(sql).fetch().one();

Как отличить

Признаки блокирующего кода:

  1. Метод не возвращает управление сразу
  2. В названии нет Async (но это не гарантия). Метод может называться doSomethingAsync, но внутри использовать get()/join(), что делает его блокирующим. И наоборот: метод без Async может возвращать CompletableFuture и быть неблокирующим.
  3. Использует get(), join(), await(), sleep()
  4. Работает с синхронными I/O

Типичные ошибки

  1. CompletableFuture.get() в async контексте: ```java CompletableFuture cf = fetchDataAsync();

// ❌ Блокирует! String result = cf.get();

// ✅ Неблокирующий cf.thenAccept(result -> process(result));


---

## 🔴 Senior Level

### Internal Implementation

**Blocking vs Non-blocking на уровне OS:**

Blocking I/O:

  • Поток переходит в состояние WAIT
  • OS переключает контекст
  • Поток не выполняет полезной работы

Non-blocking I/O:

  • Поток продолжает работу
  • Callback при завершении I/O
  • Event loop или epoll/select (Linux) ```

Thread states:

// BLOCKED — ждёт монитор
// WAITING — ждёт notify/Completion
// TIMED_WAITING — sleep, wait(timeout)

// Визуализация:
jcmd <pid> Thread.print
// или
jstack <pid>

Архитектурные Trade-offs

Подход Плюсы Минусы
Blocking Простой код Мало throughput
Non-blocking Высокий throughput Сложнее код
Virtual Threads Простой код + throughput Java 21+

Edge Cases

1. Hidden blocking:

// CompletableFuture выглядит async, но может блокировать
cf.thenApply(s -> {
    return httpClient.sendBlocking(url);  // блокирует ForkJoinPool!
});

// thenApply без Async выполняется в том же потоке, что и предыдущий этап.
// Если это ForkJoinPool.commonPool() — blocking операция блокирует один из
// немногих потоков пула → thread pool starvation для всех задач commonPool.

// ForkJoinPool имеет мало потоков — блокировка убивает производительность

2. Thread pool starvation:

// ❌ Все потоки blocked — новые задачи не выполняются
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 100; i++) {
    CompletableFuture.supplyAsync(() -> {
        Thread.sleep(10000);  // блокирует 10 секунд
        return "done";
    }, executor);
}
// Первые 10 задач блокируют все потоки
// Остальные 90 ждут

Производительность

Blocking I/O:
- 100 потоков → 100 concurrent requests
- Thread overhead: ~1MB на поток

Non-blocking I/O:
- 1 поток → 10000+ concurrent requests
- Callback overhead: ~5-10 ns

Virtual Threads:
- 100000+ потоков → миллионы concurrent requests
- Thread overhead: ~few hundred bytes
// (Java 21, approximate, зависит от workload)

Production Experience

Identifying blocking code:

# Thread dump
jstack <pid> > threads.txt

# Ищите BLOCKED, WAITING, TIMED_WAITING
# Если много потоков в этих состояниях — blocking code

# Profiling
jcmd <pid> VM.native_memory summary

Best Practices

// ✅ Async методы для I/O
httpClient.sendAsync(request, handler);

// ✅ Свой Executor для blocking операций
CompletableFuture.supplyAsync(blockingTask, ioExecutor);

// ✅ Virtual Threads (Java 21+)
Executors.newVirtualThreadPerTaskExecutor();

// ❌ get()/join() без причины
// ❌ Blocking операции в ForkJoinPool
// ❌ Игнорирование thread pool starvation

🎯 Шпаргалка для интервью

Обязательно знать:

  • Блокирующий код останавливает поток и ждёт (get(), sleep(), I/O read)
  • Неблокирующий — поток свободен, callback при завершении (CompletableFuture, NIO)
  • Признаки блокирующего: get/join/sleep/await, синхронный I/O, нет Async в названии (но не гарантия)
  • Hidden blocking: метод называется Async, но внутри get/join
  • Thread pool starvation: все потоки пула заблокированы, новые задачи ждут

Частые уточняющие вопросы:

  • Как отличить blocking от non-blocking? — blocking не возвращает управление сразу, non-blocking возвращает Future/CF сразу
  • CompletableFuture.get() блокирует? — Да, поток переходит в WAITING
  • Hidden blocking — что это? — Метод Async внутри вызывает blocking операцию, блокируя ForkJoinPool
  • Virtual Threads решают проблему? — Да, blocking suspend-ит виртуальный поток, не OS thread (Java 21+)

Красные флаги (НЕ говорить):

  • «get() в цепочке CompletableFuture — нормальная практика» — это блокировка, разрушает async
  • «Метод называется Async значит неблокирующий» — может вызывать get/join внутри
  • «Blocking в ForkJoinPool не проблема» — thread pool starvation для всех задач

Связанные темы:

  • [[15. Почему важно избегать блокирующих операций в CompletableFuture]]
  • [[12. Какой пул потоков используется по умолчанию для async методов]]
  • [[11. В чём разница между thenApply() и thenApplyAsync()]]
  • [[13. Как указать свой Executor для CompletableFuture]]