Backend

99 Ngày Java — Ngày 64: ExecutorService và thread pool

SSite Admin
29 tháng 09, 2026 2 phút đọc 1 lượt xem
99 Ngày Java — Ngày 64: ExecutorService và thread pool

Tạo một Thread cho từng công việc khiến ứng dụng khó giới hạn tài nguyên và quản lý vòng đời. ExecutorService nhận task rồi quyết định cách thực thi. Nó có thể dùng pool, một luồng hoặc virtual thread; vì vậy tên ExecutorService không đồng nghĩa với pool. Ví dụ ở đây dùng Java 21.

ExecutorService và thread pool

1. Chọn executor theo tải

newFixedThreadPool(n) tái sử dụng n worker nhưng hàng đợi mặc định không bị chặn, nên tác vụ đến nhanh hơn tốc độ xử lý có thể làm tăng bộ nhớ. newCachedThreadPool() mở thêm luồng khi cần và có thể tăng rất nhiều luồng. newSingleThreadExecutor() giữ thứ tự thực thi tác vụ nối tiếp. Với I/O blocking lớn trên Java 21, newVirtualThreadPerTaskExecutor() tạo một virtual thread cho mỗi task; đừng pool virtual thread chỉ để giới hạn số lượng, hãy giới hạn tài nguyên đích.

ExecutorService cpu = Executors.newFixedThreadPool(4);
ExecutorService serial = Executors.newSingleThreadExecutor();
ExecutorService io = Executors.newVirtualThreadPerTaskExecutor();
// Pick by workload, queue policy, and downstream capacity.

2. execute khác submit

execute(Runnable) không trả về Future. submit(Runnable/Callable) trả Future để kiểm tra hoàn tất, hủy hoặc lấy kết quả. Exception của task được submit nằm trong Future và hiện ra ở get() dưới ExecutionException; bỏ Future đi có thể khiến lỗi bị bỏ sót. Đừng dùng Future.get() ngay sau từng submit nếu muốn các task thật sự chạy song song.

Future<Integer> answer = cpu.submit(() -> 40 + 2);
// Do independent work before waiting:
try { System.out.println(answer.get()); }
catch (ExecutionException e) { throw new RuntimeException(e.getCause()); }

3. Shutdown và interrupt đúng cách

shutdown() ngừng nhận task mới nhưng cho các task đã gửi chạy tiếp. awaitTermination() đợi có giới hạn; quá hạn thì shutdownNow() yêu cầu interrupt và trả về các task còn trong hàng đợi. Task phải hợp tác xử lý interrupt như ngày 61. Luồng đang đợi mà bị interrupt nên gọi shutdownNow(), khôi phục cờ và trả quyền quyết định cho caller.

cpu.shutdown();
try {
    if (!cpu.awaitTermination(30, TimeUnit.SECONDS))
        cpu.shutdownNow();
} catch (InterruptedException e) {
    cpu.shutdownNow();
    Thread.currentThread().interrupt();
}

4. Đo trước khi chỉnh pool

Theo dõi độ dài hàng đợi, thời gian chờ, số worker và tỷ lệ lỗi. Pool nhỏ có thể nghẽn; pool quá lớn làm tăng chuyển ngữ cảnh hoặc đè quá tải database. Với ThreadPoolExecutor tùy biến, dùng queue có giới hạn và chính sách từ chối rõ ràng. Không gửi task vào chính pool rồi chờ Future của nó nếu pool có thể hết worker: deadlock do starvation.

  • Bài tập: submit nhiều task hơn số worker và quan sát kích thước hàng đợi.

  • Bài tập: cho task ngủ/chờ và gọi shutdownNow(); task đã xử lý interrupt chưa?

ExecutorService là công cụ quản lý vòng đời task, không phải một thiết lập pool phù hợp cho mọi tải. Ngày 65 sẽ dùng Future và CompletableFuture để lấy kết quả và kết hợp tác vụ bất đồng bộ.

Tài liệu đối chiếu

  • https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html

  • https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 64: Scheduling với @Scheduled

@EnableScheduling, fixedDelay/fixedRate, cron sáu trường với múi giờ và chống chạy trùng trên nhiều instance.

29 thg 9, 20263 phút0
99 Ngày Java — Ngày 63: volatile và Atomic

Phân biệt visibility với atomicity, happens-before cơ bản, volatile flag, AtomicInteger và CAS.

28 thg 9, 20263 phút5
99 Ngày Spring — Ngày 63: Application events

Publish event trong Spring, @EventListener đồng bộ, @TransactionalEventListener sau commit và giới hạn độ bền.

28 thg 9, 20262 phút6