Confinement: thread safety bằng cách không chia sẻ
Dữ liệu chỉ một thread chạm tới thì tự động thread-safe. Hai nấc confinement rẻ nhất: ad-hoc (quy ước + Swing/EDT) và stack (biến cục bộ) — khi nào đủ, khi nào vỡ.
TL;DR: Confinement triệt tiêu tính "shared": dữ liệu chỉ một thread chạm tới thì tự động thread-safe, kể cả khi class của nó không hề thread-safe. Bài này đi hai nấc đầu, từ mong manh tới cứng cáp: ad-hoc confinement chỉ dựa quy ước của lập trình viên (compiler im lặng, dễ vỡ khi một getter hay lambda phát tán tham chiếu) — nhưng nâng lên mức kiến trúc thì rất mạnh, như Swing giam mọi thao tác UI vào một thread duy nhất; và stack confinement dựa vào biến cục bộ — ngôn ngữ bảo đảm, miễn tham chiếu không rò khỏi stack frame. Né chia sẻ bao giờ cũng rẻ hơn đồng bộ hóa, nên confinement là chiến lược nên thử trước tiên.
Bài trước khép lại bằng bốn chiến lược đối phó với shared mutable state: gốc của rắc rối nằm ở hai tính từ "shared" và "mutable". Lần này ta cắt tính từ thứ nhất. Dữ liệu chỉ thuộc về một thread thì không còn "shared", và khi ấy atomicity lẫn visibility đều hết là vấn đề — confinement né hẳn câu hỏi mà khóa, happens-before và atomic class phải trả lời.
Java không có từ khóa nào khai báo "biến này thuộc về một thread", nên ta đi dọc một trục từ dạng chỉ có lời hứa của lập trình viên tới dạng ngôn ngữ đứng ra bảo đảm. Bài này lo hai nấc rẻ nhất; per-thread state kiểu "biến toàn cục nhưng riêng mỗi thread" (ThreadLocal, ScopedValue) để bài 06b.
1. Ad-hoc confinement: khi chỉ có quy ước giữ dữ liệu
Dạng yếu nhất là ad-hoc confinement: giam dữ liệu hoàn toàn bằng kỷ luật người viết. Bạn quyết định một đối tượng "chỉ được thread X dùng", viết comment nói thế, rồi tin người sau tôn trọng nó — compiler im lặng, test đơn luồng vẫn xanh, lỗi chỉ lộ dưới tải.
public class RequestProcessor {
// Quy uoc (chi la comment): 'buffer' CHI duoc dung boi thread khoi tao processor.
private final StringBuilder buffer = new StringBuilder(); // StringBuilder KHONG thread-safe
public void append(String chunk) {
buffer.append(chunk); // an toan — neu, va chi neu, quy uoc tren duoc giu
}
}
StringBuilder không thread-safe, nhưng nếu thật sự chỉ một thread gọi append thì đoạn này chạy đúng mãi mãi: đối tượng bị giam thì an toàn kể cả khi class của nó không thread-safe. Vấn đề nằm ở cụm "nếu thật sự" — quy ước không được ngôn ngữ thực thi, nên chỉ mạnh bằng người yếu nhất chạm vào code. Một getter công khai hay một lambda submit sang executor là đủ phá vỡ.
1.1 Nâng ad-hoc lên mức kiến trúc: Swing và Event Dispatch Thread
Dù vậy, một biến thể của ad-hoc đáng giá tới mức thành quyết định kiến trúc: giam cả một phân hệ vào một thread. Swing là ví dụ kinh điển — mọi thao tác lên UI component bị giam vào Event Dispatch Thread (EDT), biến bài toán đồng bộ hóa khổng lồ (hàng trăm component, nhiều event nguồn) thành một luật duy nhất: "chỉ chạm UI trên EDT".
// Cong viec nang chay tren thread nen (khong block UI)...
new Thread(() -> {
String result = fetchReport(); // I/O dai, KHONG cham UI o day
SwingUtilities.invokeLater(() ->
label.setText(result)); // day thao tac UI ve dung EDT
}).start();
SwingUtilities.invokeLater đẩy lambda vào hàng đợi event của EDT; label.setText vì thế luôn chạy trên đúng một thread. Không component Swing nào cần synchronized, vì không bao giờ có hai thread cùng chạm nó. Chạm UI từ thread khác không ném exception ngay — nó gây lỗi render lẻ tẻ, khó tái hiện, đúng kiểu bug ad-hoc confinement bị phá.
TicketFlow áp cùng tinh thần: dồn mọi yêu cầu đặt vé vào một hàng đợi cho đúng một thread tiêu thụ chạy tuần tự — events và sold bị giam trong thread đó, book không cần một dòng synchronized nào.
var requests = new LinkedBlockingQueue<BookingCommand>();
Thread worker = new Thread(() -> {
var events = new HashMap<String, Event>(); // KHONG thread-safe -- va khong can thread-safe
var sold = new HashMap<String, Integer>();
while (!Thread.currentThread().isInterrupted()) {
try {
requests.take().applyTo(events, sold); // chi worker nay doc/ghi hai map tren
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // khoi phuc co interrupt -> vong lap thoat
}
}
});
worker.start();
LinkedBlockingQueue là một BlockingQueue — hàng đợi chặn: take() trên queue rỗng chờ tới khi có phần tử thay vì trả null ngay (bài Blocking queues & producer-consumer đào sâu họ cấu trúc này). Vì take() chờ vô hạn được nên nó ném InterruptedException; worker khôi phục cờ interrupt để vòng lặp thấy cờ và thoát êm thay vì nuốt mất tín hiệu dừng — pattern cooperative cancellation của bài Thread API và vòng đời.
Hai HashMap không thread-safe ở đây hoàn toàn ổn: tạo trong thân run, không bao giờ rò ra ngoài. Vẫn là ad-hoc confinement, nhưng gói trọn state vào một thread đã thu nhỏ bề mặt sai sót. BlockingQueue là thứ duy nhất được phép chia sẻ: đặt một đối tượng vào hàng đợi thread-safe sẽ safe-publish nó, worker thấy đối tượng hoàn chỉnh chứ không phải bản dở dang vì reordering (nền móng ở bài Immutability).
2. Stack confinement: vì sao biến cục bộ tự an toàn?
Nấc tiếp theo là stack confinement: đối tượng chỉ với tới được qua biến cục bộ. Ở đây ngôn ngữ đứng về phía ta — biến cục bộ sống trên stack, mà stack là trạng thái riêng của từng thread (bài Process và Thread).
Với kiểu nguyên thủy, giam là tuyệt đối: không có cách nào lấy tham chiếu tới một biến int cục bộ. Đó là lý do SeatPriceCalculator ở bài trước an toàn dù không một dòng đồng bộ hóa. Với kiểu tham chiếu thì phải cẩn thận: tham chiếu nằm trên stack, còn đối tượng nó trỏ tới nằm trên heap dùng chung — chỉ đúng chừng nào không có tham chiếu thứ hai lọt ra ngoài thread.
public Map<String, Long> countByTier(List<Booking> bookings) {
Map<String, Long> counts = new HashMap<>(); // HashMap cuc bo — bi giam tren stack
for (Booking b : bookings) counts.merge(b.tier(), 1L, Long::sum);
return Map.copyOf(counts); // tra ra mot ban immutable, KHONG ro 'counts'
}
counts là HashMap không thread-safe, nhưng được tạo, dùng và vứt bỏ trọn trong một lần gọi; mỗi thread có một counts riêng trên stack. Đối tượng thì mutable, cách dùng lại thread-safe — nhờ vậy ta dùng được tự do các collection rẻ, không đồng bộ, giữa một chương trình đa luồng.
Cái bẫy duy nhất là để tham chiếu thoát ra. Trước khi đọc tiếp, tự trả lời: đoạn dưới counts còn bị giam trên stack không?
Method dưới tạo counts như biến cục bộ, nhưng có một dòng khiến nó hết bị giam. Dòng nào, và vì sao? Viết ra câu trả lời trước khi xem giải thích.
public void process(List<Booking> bookings) {
Map<String, Long> counts = new HashMap<>(); // tuong la bi giam...
executor.submit(() -> counts.merge("VIP", 1L, Long::sum)); // ...nhung lambda mang no sang thread khac
counts.merge("STANDARD", 1L, Long::sum); // race: hai thread cung sua 'counts'
}
Khoảnh khắc counts bị lambda chạy trên thread khác bắt giữ, nó hết bị giam — dù tên biến vẫn là biến cục bộ. Ngôn ngữ giam phần tham chiếu trên stack, còn giữ đối tượng heap không rò ra ngoài là trách nhiệm người viết. Cái bẫy này (return counts thay vì Map.copyOf(counts), gán vào field, đưa cho listener, hay để lambda bắt giữ) là cùng một lỗi ở nhiều hình dạng. Đổi lại sự cẩn thận đó, stack confinement miễn phí về hiệu năng.

Biến cục bộ kiểu nguyên thủy: giam tuyệt đối — không tồn tại cách lấy tham chiếu. Biến cục bộ kiểu tham chiếu: giam có điều kiện — object nằm trên heap, nên chỉ bị giam chừng nào không return tham chiếu sống, không gán vào field, không để lambda bắt giữ.
3. Liên hệ các bài khác
- Thread Safety — bài này thi công chiến lược thứ nhất trong bốn chiến lược bài đó gọi tên; thuật ngữ shared, mutable, race định nghĩa ở đó.
- Thread API và vòng đời — pattern khôi phục cờ interrupt mà worker ở mục 1 dùng.
- ThreadLocal và ScopedValue — nấc confinement tiếp theo: per-thread state như "biến toàn cục nhưng riêng mỗi thread", và bẫy rò rỉ trên thread pool.
- Immutability — dựng nền safe publication mà mục 1 nhắc lướt; cũng là chiến lược thứ hai, cắt vào tính từ còn lại ("mutable").
- Blocking queues & producer-consumer — đào sâu hàng đợi chặn làm cây cầu chia sẻ duy nhất giữa producer và worker bị giam.
4. 📚 Deep Dive Oracle
Spec / reference chính thức:
- Java Concurrency in Practice (Goetz et al.), §3.3 Thread Confinement — nguồn của trục ad-hoc → stack → ThreadLocal; phần ad-hoc và stack confinement là nền của bài này.
- The Event Dispatch Thread — Oracle Swing tutorial — luật "chỉ chạm UI trên EDT" và
invokeLater, ví dụ kinh điển của ad-hoc confinement mức kiến trúc.
Ghi chú: JCiP §3.3 là nguồn gốc thuật ngữ; đọc để thấy confinement là một nhánh trong bức tranh thread safety chứ không phải mẹo lẻ.
5. Tóm tắt
Confinement trả lời câu hỏi thread safety bằng cách từ chối đặt ra nó. Hai nấc rẻ nhất:
| Nấc | Ai đứng ra bảo đảm | Mong manh ở đâu |
|---|---|---|
| Ad-hoc | Quy ước + kỷ luật người viết | Một getter hay lambda phát tán tham chiếu là vỡ, compiler im lặng |
| Stack | Ngôn ngữ — stack riêng từng thread | Đối tượng heap rò khỏi stack frame (return sống, field, lambda bắt giữ) |
- Dữ liệu chỉ một thread chạm tới thì tự động thread-safe, kể cả khi class không thread-safe — confinement né cả atomicity lẫn visibility.
- Ad-hoc chỉ dựa quy ước; mạnh nhất khi nâng lên mức kiến trúc (Swing/EDT, single-consumer queue) giam cả một phân hệ vào một thread.
- Stack: biến cục bộ nguyên thủy giam tuyệt đối; biến tham chiếu giam có điều kiện — object heap vỡ nếu tham chiếu rò khỏi stack frame.
- Confinement trong phạm vi hẹp là đủ khi ta thật sự không chia sẻ. Khi cần thứ vừa "toàn cục trong tầm với" vừa riêng mỗi thread, đó là
ThreadLocal/ScopedValue— bài tiếp theo.
6. Tự kiểm tra
- Q1Vì sao ad-hoc confinement mong manh, và điều gì biến nó từ mẹo dễ vỡ thành quyết định kiến trúc vững như trong Swing?
- Q2Trong countByTier, một HashMap không thread-safe được dùng giữa chương trình đa luồng mà vẫn an toàn tuyệt đối. Vì sao? Điều gì sẽ phá vỡ bảo đảm đó?
- Q3Vì sao biến cục bộ kiểu nguyên thủy được giam tuyệt đối, còn biến cục bộ kiểu tham chiếu chỉ giam có điều kiện?
- Q4Trong worker single-threaded của TicketFlow, vì sao requests.take() phải bọc try/catch InterruptedException, và vì sao trong catch lại gọi Thread.currentThread().interrupt()?
- Q5Vì sao đặt một BookingCommand vào LinkedBlockingQueue lại là cách chia sẻ an toàn, trong khi chia sẻ trực tiếp một object mutable giữa hai thread thì không?
Bài tiếp theo: ThreadLocal và ScopedValue — per-thread state không leak trên pool
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