Exchanger và Phaser — điều phối tiến độ ngoài ba máy đếm
Hai synchronizer ít gặp: Exchanger cho rendezvous đúng hai thread đổi dữ liệu, Phaser cho barrier đa pha số party động — và bẫy arrive* chỉ báo hiệu, không chờ.
TL;DR: Ngoài ba máy đếm, java.util.concurrent còn hai synchronizer cho hình dạng hiếm hơn. Exchanger<T> là điểm hẹn cho đúng hai thread đổi dữ liệu cho nhau: mỗi bên gọi exchange(x), block tới khi bên kia tới, rồi cả hai nhận giá trị của nhau — hợp nhất với double buffering, còn lại BlockingQueue thường tổng quát hơn. Phaser (Java 7) tổng quát hóa cả latch lẫn barrier: lặp theo pha và cho thread gia nhập/rời đi giữa chừng bằng register/arriveAndDeregister. Bẫy lớn nhất: họ arrive* chỉ báo hiệu rồi return ngay, không chờ — muốn chờ phải gọi awaitAdvance(phase).
Bài trước mổ ba synchronizer đếm — CountDownLatch, CyclicBarrier, Semaphore — mỗi cái điều phối nhiều thread bằng một con số. Hai hình dạng không vừa khuôn đó: đổi dữ liệu giữa đúng hai thread, và một barrier mà số thành viên thay đổi giữa chừng. Đó là chỗ của Exchanger và Phaser.
1. Exchanger: trao đổi dữ liệu giữa đúng hai thread
Ba máy đếm ở bài trước điều phối nhiều thread theo số đếm. Exchanger<T> là ca rất hẹp: điểm hẹn cho đúng hai thread đổi dữ liệu cho nhau. Method đáng kể duy nhất là exchange(x) — thread gọi block tới khi một thread thứ hai gọi exchange(y) trên cùng đối tượng; lúc đó bên đầu nhận y, bên kia nhận x, cả hai đi tiếp. Hình dung hai điệp viên trao vali ở ga: ai tới trước đứng chờ, trao đổi chỉ xong khi cả hai có mặt — một rendezvous đối xứng.
Thực dụng mà nói, Exchanger ít gặp nhất trong bộ năm: đa số bài toán "đưa dữ liệu từ thread này sang thread kia" được BlockingQueue ở bài blocking queues phục vụ tốt và tổng quát hơn (không giới hạn hai thread, không bắt cả hai phía có mặt cùng lúc). Biết nó tồn tại, nhận ra hình dạng hợp với nó, là đủ.
Mẫu kinh điển của Exchanger là producer–consumer dùng double buffering: producer đổ đầy buffer A trong khi consumer rút cạn buffer B, rồi hai bên cùng gọi exchange(buffer) để tráo - producer nhận lại buffer đã rỗng để đổ tiếp, consumer nhận buffer đầy để xử lý. So với đẩy từng phần tử qua BlockingQueue, cách này trao cả khối dữ liệu một lúc và tái dùng chính các buffer, tránh cấp phát liên tục - hợp cho pipeline thông lượng cao, nhạy cảm với áp lực GC. Exchanger chỉ thực sự thắng khi bài toán đối xứng giữa đúng hai bên và ta muốn tái sử dụng vật chứa.
2. Phaser: barrier đa pha với số party động
CountDownLatch đếm một lần rồi thôi; CyclicBarrier lặp lại nhưng số party cố định cứng từ lúc khởi tạo — cả hai đều đòi số "người tham gia" biết trước và không đổi. Phaser (Java 7) gỡ cả hai hạn chế: vừa lặp theo pha như barrier, vừa cho thread gia nhập và rời đi giữa chừng. Hình dung một cuộc họp nhiều phiên nơi người dự được đến muộn hoặc về sớm: mỗi phiên chỉ kết thúc khi mọi người đang có mặt xong việc, mà danh sách đó thay đổi giữa các phiên. Số party là đại lượng động — register() để vào, arriveAndDeregister() để ra.
public class DynamicPipeline {
private final Phaser phaser = new Phaser(1); // "1" = thread dieu phoi tu dang ky
public void addWorker(Runnable task, int phases) {
phaser.register(); // worker moi gia nhap, party + 1
Thread.startVirtualThread(() -> {
try {
for (int p = 0; p < phases; p++) {
task.run();
phaser.arriveAndAwaitAdvance(); // bao "xong pha", cho party khac
}
} finally {
phaser.arriveAndDeregister(); // rut khoi nhom, party - 1
}
});
}
}
Vốn từ của Phaser ánh xạ thẳng sang cái đã biết: arriveAndAwaitAdvance() chính là CyclicBarrier.await(), còn register() / arriveAndDeregister() là phần barrier không có (đổi sĩ số nhóm tại chỗ). Nó cũng tái hiện được hành vi kiểu CountDownLatch — nên là thứ gói được cả hai, đổi lại bằng một API rậm rạp hơn.
Điểm dễ hiểu lầm nhất: API của Phaser tách đôi thứ mà CyclicBarrier.await() gộp làm một.
- Họ
arrive*là nửa báo hiệu — đánh dấu "tôi đã tới mốc" rồi đi tiếp ngay, không chặn. awaitAdvance(phase)là nửa chờ — block tới khi pha mang sốphasekhép lại.arriveAndAwaitAdvance()gộp sẵn cả hai (báo rồi chờ) — nên nó tương đươngCyclicBarrier.await().
flowchart TB
subgraph SIGNAL["Nua BAO HIEU: return NGAY, khong chan"]
A1["arrive()"]
A3["arriveAndDeregister()"]
end
subgraph WAIT["Nua CHO: block toi khi pha khep"]
W1["awaitAdvance(phase)"]
end
COMBO["arriveAndAwaitAdvance()<br/>= bao + cho, gop san"]
A1 -.->|"can cho thi hung phase roi goi"| W1
A3 -.->|"can cho thi hung phase roi goi"| W1
style COMBO fill:#93C5FD
style W1 fill:#6EE7B7Còn một nấc nữa: override onAdvance(int phase, int registeredParties) để can thiệp "khi nào pha kết thúc" — trả về true sẽ kết liễu phaser, diễn đạt điều kiện dừng của thuật toán lặp ("chạy tới khi hội tụ" thay vì "đúng K pha"), điểm linh hoạt mà latch lẫn barrier đều không cho. Phaser cũng tự terminate khi worker cuối đưa sĩ số về 0.
Khi nào với tay tới Phaser? Khi số thread tham gia thay đổi trong vòng đời tính toán, hoặc điều kiện dừng phụ thuộc trạng thái chứ không phải số vòng cố định — nhóm cố định, bài toán đơn giản thì CountDownLatch/CyclicBarrier dễ đọc hơn, đừng trả giá tính động khi không cần.
Spec / reference chính thức:
Phaserjavadoc (Java 21) — đặc tả đầy đủ arrive/await/register,onAdvancevà termination; phần "Sample usages" minh họa các idiom chuẩn.Exchangerjavadoc (Java 21) — đặc tảexchange, và ví dụ double-buffering trong phần mô tả class.- Java Concurrency in Practice — Goetz et al., chương 5.5 "Synchronizers" — khung phân loại latch/barrier/semaphore/exchanger mà bài này dựa vào.
3. Vì sao một method "await" lại không hề chờ?
Ta vừa chốt: họ arrive* chỉ báo hiệu, awaitAdvance mới chờ. Giờ dùng phân biệt đó để bắt một bug ngữ nghĩa mà trình biên dịch vẫn cho qua. Một service muốn phơi ra method awaitFirstPhase() — chặn caller tới khi pha đầu tiên của pipeline khép lại — và ai đó viết thế này:
public int awaitFirstPhase() {
return phaser.arriveAndDeregister(); // TODO: co that su cho khong?
}
Thân method chỉ có một dòng arriveAndDeregister(), rồi trả về giá trị nó cho. Method này có thực sự chờ pha đầu khép lại trước khi return không? Viết ra câu trả lời (có / không) kèm một dòng lý do — dựa vào phân biệt báo-hiệu vs chờ ở trên — rồi mới đọc tiếp.
arriveAndDeregister() thuộc nửa báo hiệu: nó đánh dấu "tôi tới mốc và rút khỏi nhóm" rồi return ngay, mang theo số pha hiện tại — không chờ ai. Nên method trên trả về tức thì, chưa chắc pha đầu đã khép. Đó là bug ngữ nghĩa: tên hứa "await" nhưng thân chỉ "arrive". Trình biên dịch không thể bắt vì kiểu trả về (int số pha) vẫn hợp lệ — chỉ hành vi lúc chạy mới sai.
Sửa thế nào? Hứng số pha mà arriveAndDeregister() trả về, rồi đưa cho awaitAdvance — lời gọi này mới chặn tới khi pha đó khép lại:
public int awaitFirstPhase() {
int phase = phaser.arriveAndDeregister(); // bao "toi moc + rut", nhan so pha
return phaser.awaitAdvance(phase); // CHO toi khi pha do khep lai
}
Quy tắc rút ra để áp cho mọi lần dùng Phaser: khi cần chờ, phải có một lời gọi thuộc họ await (awaitAdvance, awaitAdvanceInterruptibly) hoặc dùng thẳng arriveAndAwaitAdvance(). Nếu code chỉ có arrive* mà kỳ vọng nó chặn, đó là bug — và nó sẽ không lộ ra tới khi một pha chạy nhanh hơn dự tính.
4. Liên hệ các bài khác
- Synchronizers: CountDownLatch, CyclicBarrier, Semaphore — ba máy đếm mà
Phasertổng quát hóa; đọc để thấyarriveAndAwaitAdvance()chính làCyclicBarrier.await()và vì saoPhasergói được cả latch. - Blocking Queues & Producer–Consumer —
BlockingQueuelà lựa chọn mặc định để chuyển dữ liệu giữa thread;Exchangerchỉ thắng ở ca đối xứng đúng hai bên (double buffering). - Thread API và vòng đời —
exchange,arriveAndAwaitAdvance,awaitAdvanceđều là lời gọi block phản hồi interrupt; bài đó dạy xử lýInterruptedExceptioncho đúng. - Executor & thread pool — bài kế tiếp mở Phần B: thôi tự tạo thread, bắt đầu tổ chức công việc thành task.
5. Tóm tắt
Exchanger và Phaser phủ hai hình dạng điều phối mà ba máy đếm không vừa khuôn:
| Synchronizer | Câu hỏi nó trả lời | Tái sử dụng | Chờ bằng gì |
|---|---|---|---|
Exchanger | "Bên kia tới điểm hẹn chưa?" | Mỗi lần ghép một cặp | exchange() tới khi đủ hai bên |
Phaser | "Pha hiện tại khép chưa?" | Đa pha, party động | awaitAdvance(phase) |
Exchanger hẹp và hiếm — nhớ nó tồn tại, nhận ra double buffering là hình dạng của nó, là đủ. Phaser mạnh nhưng API rậm: chỉ trả giá độ phức tạp khi số party động hoặc điều kiện dừng phụ thuộc trạng thái; và luôn nhớ họ arrive* chỉ báo hiệu, muốn chờ phải awaitAdvance.
Đến đây khép lại Phần A của series — năm synchronizer, năm hình dạng điều phối, đều là component đã kiểm chứng trong java.util.concurrent, việc của ta là chọn đúng cái cho hình dạng bài toán chứ không dựng lại bằng wait/notify. Nhưng tới giờ ta vẫn tự tay tạo thread khắp nơi (Thread.startVirtualThread(...) rải trong gần như mọi ví dụ). Câu hỏi tiếp theo không còn là "giữ state đúng" mà là "tổ chức và chạy công việc concurrent có kỷ luật" — ai tạo thread, bao nhiêu, tái dùng và dừng thế nào. Đó là Phần B, mở màn bằng Executor và thread pool: thôi nghĩ về thread, bắt đầu nghĩ về task.
6. Tự kiểm tra
Q1arriveAndAwaitAdvance() và arriveAndDeregister() khác nhau thế nào? Vì sao một method tên "await" mà thân chỉ gọi arriveAndDeregister() là bug?▸
arriveAndAwaitAdvance() và arriveAndDeregister() khác nhau thế nào? Vì sao một method tên "await" mà thân chỉ gọi arriveAndDeregister() là bug?Phaser tách đôi thứ mà CyclicBarrier.await() gộp làm một: họ arrive* là nửa báo hiệu — đánh dấu đã tới mốc rồi return ngay; awaitAdvance(phase) là nửa chờ — block tới khi pha đó khép lại. arriveAndAwaitAdvance() gộp sẵn cả hai (báo rồi chờ), còn arriveAndDeregister() chỉ báo và rút khỏi nhóm, không chờ ai. Method "await" mà chỉ gọi nó sẽ return tức thì — bug ngữ nghĩa compile vẫn xanh. Cặp đúng: int phase = phaser.arriveAndDeregister(); phaser.awaitAdvance(phase);.Q2Khi nào Exchanger thắng BlockingQueue cho việc chuyển dữ liệu giữa thread? Vì sao đa số trường hợp lại chọn BlockingQueue?▸
Exchanger chỉ thắng khi bài toán đối xứng đúng hai bên và ta muốn tái sử dụng vật chứa — điển hình double buffering: producer đổ đầy buffer A, consumer rút cạn buffer B, hai bên exchange(buffer) để tráo, tránh cấp phát liên tục (hợp pipeline nhạy GC). Đa số trường hợp chọn BlockingQueue vì tổng quát hơn: không giới hạn hai thread, không bắt cả hai phía có mặt cùng lúc, tách rời producer với consumer về nhịp độ — trong khi Exchanger bắt cả hai tới điểm hẹn mới xong.Q3register() và arriveAndDeregister() cho Phaser khả năng gì mà CyclicBarrier thiếu? Cho một ví dụ.▸
register() và arriveAndDeregister() cho Phaser khả năng gì mà CyclicBarrier thiếu? Cho một ví dụ.register() (party + 1), rút lui bằng arriveAndDeregister() (party - 1) — trong khi CyclicBarrier chốt cứng số party lúc khởi tạo. Ví dụ: một pipeline động nơi worker được thêm theo tải, mỗi worker chạy vài pha rồi tự rút; barrier không thể tăng/giảm số "người phải tới mốc", còn phaser thì mỗi lần register/deregister tự cập nhật ngưỡng "đủ mặt" (và tự terminate khi party về 0).Q4Override onAdvance(int phase, int registeredParties) trả về true làm gì? Nó cho Phaser khả năng gì mà latch/barrier không có?▸
onAdvance(int phase, int registeredParties) trả về true làm gì? Nó cho Phaser khả năng gì mà latch/barrier không có?onAdvance chạy đúng một lần mỗi khi một pha khép lại (giống chỗ của barrier action); trả về true sẽ kết liễu phaser. Khả năng nó mở ra: điều kiện dừng phụ thuộc trạng thái chứ không phải số vòng cố định — "chạy tới khi hội tụ" thay vì "đúng K pha". Latch chỉ đếm về 0, barrier lặp mãi tới khi ta ngừng gọi await(); cả hai đều không có móc này, nên phaser hợp thuật toán lặp mà số vòng không biết trước.Q5Khi nào đáng trả giá độ phức tạp API của Phaser thay vì dùng CountDownLatch hay CyclicBarrier?▸
register() / arriveAndDeregister()), hoặc điều kiện dừng phụ thuộc trạng thái thay vì số vòng cố định (onAdvance trả về true để kết liễu — "chạy tới khi hội tụ"). Nếu nhóm cố định và số pha biết trước, latch/barrier dễ đọc hơn hẳn — đừng trả giá tính động và API rậm rạp của phaser khi bài toán không cần nó.Bài tiếp theo: Executor Framework — thread pool và thuật toán nhận task
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên