Питання 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]]