Thread, Runnable, Callable và Future
Callable này sẽ chạy trên main thread?
public class Main {
public static void main(String[] args) throws Exception {
Callable<Integer> task = () -> {
return 10 + 20;
};
Integer result = task.call();
System.out.println(result);
}
}
Đúng, đoạn này chạy trên main thread. call() chỉ là một method bình thường: thread nào gọi call() thì thread đó chạy code bên trong.
main thread
|-- tạo Callable task (chưa chạy gì cả)
|-- task.call()
| └── return 10 + 20
|-- result = 30
└-- println(30)
Bản thân Callable (và Runnable) không tự tạo thread, nó chỉ mô tả công việc. Để thấy vì sao cần đưa nó sang thread khác, mình quay lại Order Service.
1. Bài toán
Order Service có API GET /checkout/{userId} cần 3 thông tin, mỗi cái gọi một service khác:
- Lấy thông tin user: ~1s
- Kiểm tra tồn kho (Inventory): ~1s
- Tính phí ship: ~1s
Ba việc này độc lập nhau. Tomcat thread nhận request rồi gọi lần lượt.
2. Vấn đề
Tomcat thread exec-1:
getUser ████ 1s ████
checkStock ████ 1s ████
shippingFee ████ 1s ████
→ tổng 3s, trong khi 3 việc hoàn toàn chạy cùng lúc được
Muốn chạy song song thì phải có thêm thread. Và việc chạy ở thread khác thường cần trả kết quả về cho thread request.
3. Giải pháp: tách "việc cần làm" khỏi "thread chạy nó"
Thread: thứ duy nhất thực sự chạy được. Gọistart()để tạo luồng mới.Runnable: mô tả việc,void run(), không trả kết quả.Callable<V>: mô tả việc,V call() throws Exception, có kết quả.Future<V>: "phiếu nhận kết quả" khi submit Callable vàoExecutorService.
4. Code từng bước
Bước 1: hai cách tạo thread.
// Cách 1: kế thừa Thread (ít dùng)
class MyThread extends Thread {
@Override public void run() { System.out.println("chạy ở " + getName()); }
}
new MyThread().start();
// Cách 2: truyền Runnable (hay dùng)
Runnable task = () -> System.out.println("chạy ở " + Thread.currentThread().getName());
new Thread(task).start();
Nên dùng cách 2: tách việc khỏi thread. Java không đa kế thừa, nên kế thừa Thread làm class của bạn mất cơ hội kế thừa class khác.
Bước 2: start() khác run().
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // in "main": gọi method thường, KHÔNG có thread mới
t.start(); // in "Thread-0": tạo thread mới, thread mới gọi run()
t.start(); // IllegalThreadStateException: một thread chỉ start được một lần
Bước 3: cần kết quả → Callable + ExecutorService + Future.
@Service
public class CheckoutService {
private static final ExecutorService POOL = Executors.newFixedThreadPool(10);
public CheckoutDto checkout(long userId) throws Exception {
Future<User> userF = POOL.submit(() -> userClient.get(userId));
Future<Stock> stockF = POOL.submit(() -> inventoryClient.check(userId));
Future<Integer> feeF = POOL.submit(() -> shippingClient.fee(userId));
// submit HẾT rồi mới get, luôn có timeout
User u = userF.get(2, TimeUnit.SECONDS);
Stock s = stockF.get(2, TimeUnit.SECONDS);
int fee = feeF.get(2, TimeUnit.SECONDS);
return new CheckoutDto(u, s, fee);
}
}
Bước 4: test thread nào chạy.
Callable<String> who = () -> Thread.currentThread().getName();
System.out.println(who.call()); // "main": gọi trực tiếp như method thường
System.out.println(POOL.submit(who).get()); // "pool-1-thread-1": executor chạy giùm
5. Cách hoạt động
Tomcat exec-1 (request /checkout)
│ submit(getUser) ──► pool-1-thread-1: getUser ████ 1s ████
│ submit(checkStock) ──► pool-1-thread-2: checkStock████ 1s ████
│ submit(fee) ──► pool-1-thread-3: fee ████ 1s ████
│ userF.get() ... WAITING (block) ...
│ stockF.get() → có ngay
│ feeF.get() → có ngay
▼ trả response → tổng ≈ 1s thay vì 3s
Khi submit, call() chạy trên thread của pool. Thread request đi tiếp, cho tới khi gọi future.get() thì bị block chờ kết quả. Nhiều bạn nhầm chỗ này: thấy main "đứng chờ" nên tưởng Callable chạy trên main. Còn nếu submit xong gọi get() ngay thì gần như chẳng song song gì.
6. So sánh
| Runnable | Callable | |
|---|---|---|
| Method | void run() | V call() throws Exception |
| Trả kết quả | Không | Có |
| Checked exception | Không ném được | Ném được |
| Chạy bằng | new Thread(r).start() hoặc executor | Executor (submit, invokeAll) |
Gọi trực tiếp run()/call() | Chạy trên thread hiện tại, không có thread mới | |
Nhân tiện: execute(Runnable) không trả gì; submit() nhận cả Runnable lẫn Callable và trả Future. Lỗi trong task của submit() bị giữ trong Future, không gọi get() thì không ai thấy.
7. Khi nào nên dùng / không nên dùng
- Runnable: việc "bắn rồi quên", không cần kết quả (ghi log, gửi email).
- Callable + Future: cần kết quả, cần biết lỗi, có thể chờ có timeout.
- Không nên:
new Thread()cho mỗi request (không giới hạn), gọiget()không timeout, gọirun()tưởng là chạy song song. - Cần nối tiếp hoặc ghép kết quả mà không block:
CompletableFuture(bài 3.9).
new Thread(runnable) thì có, nhưng new Thread(callable) thì không. Vậy Callable muốn chạy thread mới thì bắt buộc dùng ExecutorService?Quay lại câu hỏi ban đầu, mình tóm lại thế này có đúng không:
CallablevàRunnableđều là functional interface; method củaCallabletrả về được kết quả, với nhiều kiểu dữ liệu khác nhau.Threadcó constructor nhậnRunnablenhưng không có choCallable.Runnablechạy trên thread mới bằngnew Thread(r).start(), cònCallablemuốn chạy trên thread khác thì phải quaExecutorService/ thread pool?
Đúng gần hết. Chỉ chỉnh một chút ở ý "trả về nhiều kiểu":
- Cả hai là functional interface: mỗi cái chỉ có 1 abstract method nên viết được bằng lambda.
- Callable trả về nhiều kiểu nhờ Generic, nhưng mỗi
Callable<V>có một kiểu cố định. Đã chọnV = Integerthìcall()thànhInteger call(); trả"Hello"là lỗi compile. - Thread chỉ nhận Runnable:
new Thread(callable)không compile. - Callable chạy thread khác thường qua ExecutorService: đúng, đây là cách chuẩn.
Callable<Integer> c1 = () -> 100;
Callable<String> c2 = () -> "Hello";
Callable<Integer> bad = () -> "Hello"; // ❌ compile error
Callable<V> ──submit──> ExecutorService ──trả về──> Future<V> ──get()──> V
Thực ra vẫn có cách không cần pool: bọc vào FutureTask (vì nó là Runnable) rồi đưa cho new Thread(...). Nhưng trong code thật thì hầu như luôn dùng executor.
Mình hiểu Future là "phiếu nhận kết quả". Đi sâu hơn thì bên trong nó là gì?
Đúng, "phiếu nhận kết quả" là chuẩn. Đi sâu thêm một tầng: Future chỉ là interface (get, isDone, cancel, isCancelled). Object thật đứng sau thường là FutureTask, vừa là Runnable vừa là Future:
// submit() bên trong gần giống:
FutureTask<Integer> ft = new FutureTask<>(callable);
execute(ft); // FutureTask là Runnable → worker chạy được
return ft; // FutureTask cũng là Future → trả về cho bạn
// FutureTask<V> implements RunnableFuture<V>
// RunnableFuture<V> extends Runnable, Future<V>
Callable ─bọc─► FutureTask ─(là Runnable)─► Worker thread chạy ft.run()
│
└─(là Future)─► trả cho bạn: get() / isDone() / cancel()
Khi worker chạy ft.run(), bên trong (đơn giản hóa) là:
try {
result = callable.call(); // lưu kết quả
state = NORMAL; // đánh dấu xong, đánh thức thread đang get()
} catch (Throwable e) {
exception = e; // lưu lỗi, get() sẽ ném ExecutionException
}
Nếu thread request gọi get() trước khi worker xong, nó bị park (ngủ chờ) trong FutureTask, worker xong thì đánh thức nó.
Cho mình một bài toán và code mẫu: cần làm 10 việc, thread pool có 5 thread (khai báo static dùng chung cho app), mỗi việc là một Callable trả về kết quả. Function phải đợi đủ 10 kết quả rồi mới tính output cuối.
Được. Bài toán: Order Service có báo cáo tổng doanh thu 10 cửa hàng, mỗi cửa hàng query mất ~1 giây, app có pool chung 5 thread.
public class RevenueService {
// pool dùng chung cho cả app, không tạo mới mỗi lần gọi
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(5);
public static double calculateTotalRevenue() throws Exception {
List<Future<Double>> futures = new ArrayList<>();
// 1. Tạo và submit 10 task
for (int storeId = 1; storeId <= 10; storeId++) {
final int id = storeId;
futures.add(EXECUTOR.submit(() -> getRevenue(id)));
}
// 2. Chờ và lấy kết quả của cả 10 task
double total = 0;
for (Future<Double> f : futures) {
total += f.get();
}
// 3. Có đủ 10 kết quả rồi mới return
return total;
}
private static double getRevenue(int storeId) throws InterruptedException {
System.out.println("Store " + storeId + " on " + Thread.currentThread().getName());
Thread.sleep(1000); // giả lập query DB
return storeId * 1000.0;
}
}
Task 1..5 → worker 1..5 chạy ngay
Task 6..10 → nằm trong queue, chờ worker rảnh
Tổng thời gian ≈ 2 giây (2 đợt), tuần tự sẽ là 10 giây
future.get() chỉ trả số, làm sao biết số đó của cửa hàng nào?Hai câu hỏi về ví dụ này:
future.get()chỉ trả về kết quả, mình không biết future đó là của việc thứ mấy. Vậy phải sửa code lúc tạoCallable?- Bỏ
finalởint id = storeId;thì có được không?
Đúng, bạn đang nhìn ra hạn chế của ví dụ trước. Cách nên dùng: cho Callable trả về object chứa cả ID và kết quả, đừng dựa vào index của List:
record StoreRevenue(int storeId, double revenue) {}
List<Future<StoreRevenue>> futures = new ArrayList<>();
for (int storeId = 1; storeId <= 10; storeId++) {
int id = storeId; // không cần final, miễn không gán lại
futures.add(EXECUTOR.submit(() -> new StoreRevenue(id, getRevenue(id))));
}
for (Future<StoreRevenue> f : futures) {
StoreRevenue r = f.get();
System.out.println("Store " + r.storeId() + " = " + r.revenue());
}
Về final: bỏ được hoàn toàn. Lambda chỉ cần biến effectively final, tức là sau khi gán thì không đổi nữa.
int id = storeId;
Callable<Double> ok = () -> getRevenue(id); // OK
id++; // từ đây id không còn effectively final
Callable<Double> bad = () -> getRevenue(storeId); // ❌ storeId bị ++ trong vòng for
Phỏng vấn hỏi "Runnable khác Callable thế nào, Callable có chạy trên main thread không?" thì trả lời sao?
Câu 1: Runnable khác Callable thế nào?
🇻🇳 Tiếng Việt:
Runnablecórun()trả void, không ném checked exception, đưa được vàonew Thread().Callable<V>cócall()trả kết quả kiểu V và ném được exception, thường submit vàoExecutorServiceđể nhậnFuture<V>. Executor bọc Callable thànhFutureTask, vừa là Runnable để worker chạy, vừa là Future để mình lấy kết quả.
🇬🇧 English:
Runnablehasvoid run(), cannot throw checked exceptions and can be passed tonew Thread().Callable<V>hascall()that returns a V and may throw; we usually submit it to anExecutorServiceand get aFuture<V>. Internally the executor wraps it in aFutureTask, which is both a Runnable for the worker and a Future for the caller.
Câu 2: Callable có chạy trên main thread không?
🇻🇳 Tiếng Việt:
Tùy cách gọi. Callable chỉ mô tả công việc, không tự tạo thread: gọi trực tiếp
call()thì chạy trên thread hiện tại, submit vào executor thì chạy trên worker của pool. Main chỉ bị block khi gọifuture.get(), nên em submit hết task trước rồi mới get, có timeout. Tương tự, gọithread.run()không tạo thread mới, phải gọistart().
🇬🇧 English:
It depends on how it is invoked. A Callable only describes work and never creates a thread: calling
call()directly runs it on the current thread, submitting it runs it on a pool worker. The caller only blocks onfuture.get(), so I submit all tasks first and then get with a timeout. Likewisethread.run()does not start a new thread;start()does.
Câu 3: Future có nhược điểm gì?
🇻🇳 Tiếng Việt:
Futurechỉ cóget()(block),isDone(),cancel(); không gắn callback, không ghép nhiều kết quả. Cần nối tiếp hay ghép thì em dùngCompletableFuturevới executor riêng, vì không truyền executor thì nó dùng common ForkJoinPool rất ít thread.
🇬🇧 English:
Futureonly offers blockingget(),isDone()andcancel(); no callbacks and no way to combine results. For chaining or combining I useCompletableFuturewith a dedicated executor, because without one it falls back to the small common ForkJoinPool.
Cách nhớ ngắn gọn
- Runnable/Callable = tờ giấy ghi việc. Thread = người làm.
run()/call()gọi tay → chạy ngay tại chỗ.start()/submit()→ thread khác.- Callable ─submit─► Future (thật ra là FutureTask) ─get()─► kết quả.
- Submit hết rồi mới get, get luôn có timeout.
Tự kiểm tra
Gọi start() hai lần trên cùng một Thread thì sao?
Lần hai ném IllegalThreadStateException. Thread đã start không thể start lại.
who.call() gọi trực tiếp trong main in ra gì?
"main". Gọi trực tiếp là method call thường, không có thread mới.
Task trong Callable ném exception thì main thấy ở đâu?
Khi gọi future.get(), exception được bọc trong ExecutionException; lấy lỗi gốc bằng getCause().
Vì sao executor chạy được Callable dù Thread chỉ nhận Runnable?
Nó bọc Callable vào FutureTask, class implement RunnableFuture = Runnable + Future.
Vì sao không nên gọi API chậm bằng supplyAsync mà không truyền executor?
Nó dùng common ForkJoinPool rất ít thread và dùng chung toàn JVM; task I/O block sẽ làm nghẽn mọi thứ khác dùng pool đó.