Initialization safety của final field — freeze action
Vì sao final field hiển thị đúng cho mọi thread kể cả khi publish qua data race: freeze action JLS §17.5, this không escape, và mẫu immutable holder.
TL;DR: Initialization safety là bảo đảm bộ nhớ mà final field cho không: nếu object được construct đúng cách (this không escape khỏi constructor), mọi thread thấy tham chiếu tới nó đều chắc chắn thấy các final field mang giá trị đã gán — kể cả khi publish qua một field thường, không volatile, không khóa. Cơ chế là freeze action (JLS §17.5): cuối constructor, các final field được "đóng băng" bằng một hàng rào bộ nhớ, cấm reorder khiến reader thấy object nửa vời. Object mutable không có đặc quyền này nên phải safe-publish; khi chỉ có "effectively immutable", mẫu immutable holder sau một tham chiếu volatile là cách công bố state mới an toàn.
Bài trước dừng lại ở một lời hứa: final không chỉ cấm gán lại mà còn mang một bảo đảm về bộ nhớ mà Java Memory Model cho không — ngoại lệ hiếm hoi của quy tắc safe publication. Bài này trả nợ lời hứa đó, và cho thấy phải làm gì khi bạn không có đặc quyền của final.
Một phép ẩn dụ để neo trực giác: gửi một kiện hàng. Object không final giống vừa nhét hàng vào thùng vừa dán nhãn địa chỉ song song — người giao có thể chộp thùng đi ngay khi nhãn vừa dán, lúc hàng bên trong chưa đủ. final field thì như một quy trình có kỷ luật: đóng đủ hàng → niêm phong thùng → rồi mới cho phép gửi đi; ai nhận thùng cũng chắc chắn thấy hàng đầy đủ.
| Đời thường | Initialization safety |
|---|---|
| Kiện hàng | Object đang construct |
| Nhét hàng vào thùng | Ghi final field trong constructor |
| Niêm phong thùng trước khi gửi | Freeze action cuối constructor |
| Người nhận mở thùng, hàng đủ | Thread khác thấy final field đúng giá trị |
| Vừa nhét vừa dán nhãn, không niêm phong | Publish object không final → thấy nửa vời |
1. Điều gì xảy ra khi publish object qua một data race?
Bài Visibility đã dựng nên vấn đề: thread tạo object rồi công bố tham chiếu cho thread khác mà không đồng bộ hóa, thread thứ hai có thể thấy tham chiếu đã gán nhưng bắt gặp object ở trạng thái chưa hoàn tất, vì ghi field và ghi tham chiếu có thể bị reorder. Đó là lý do object mutable cần được safe-publish: bằng volatile, bằng khóa, hay bằng một cấu trúc concurrent.
Trong đoạn dưới, một thread ghi holder = new Holder(42), thread kia chờ tham chiếu xuất hiện rồi đọc holder.value. Field value KHÔNG khai báo final, và holder là field thường (không volatile, không khóa). Reader có thể in ra những giá trị nào? Viết ra dự đoán của bạn trước khi đọc tiếp.
static class Holder {
int value; // KHONG final -- khong co freeze guarantee
Holder(int v) { this.value = v; }
}
static Holder holder; // field thuong: khong volatile, khong lock
public static void main(String[] args) { // hai thread: mot ghi, mot doc
new Thread(() -> {
while (holder == null) { } // doi tham chieu xuat hien
System.out.println(holder.value); // doc va in holder.value
}).start();
holder = new Holder(42); // unsafe publication
}
Theo JMM, dòng holder = new Holder(42) thực chất là một chuỗi: cấp phát bộ nhớ, ghi value = 42, gán tham chiếu vào holder — hai bước sau được phép hoán đổi thứ tự. Reader vì thế có thể bắt tham chiếu trước khi value được ghi, rồi in ra 0 thay vì 42. Trên x86 chạy nghìn lần cũng khó tái hiện; chính sự "gần như luôn đúng" đó khiến lỗi hiểm — nó hợp lệ theo spec, chỉ lộ trên CPU yếu về ordering hoặc đúng lần JIT tối ưu mạnh tay.
2. Freeze action — đặc quyền JLS §17.5 của final field
JLS §17.5 (final field semantics) trao cho final field một đặc quyền: nếu object được construct đúng cách — this không escape khi constructor còn chạy — thì mọi thread nhìn thấy tham chiếu tới nó đều chắc chắn thấy các final field mang giá trị đã gán trong constructor, không cần đồng bộ hóa. Cơ chế đó tên là freeze action: theo §17.5.1, freeze của một final field xảy ra khi constructor kết thúc (dù kết thúc bình thường hay do exception). Về hiện thực, JVM chèn một hàng rào bộ nhớ (store-store barrier) giữa các lần ghi final field và lần ghi tham chiếu công bố object, cấm đúng kiểu reorder đã làm ví dụ trên in ra 0.
flowchart TB
subgraph plain["Field thuong - unsafe publication"]
W1["Writer: ghi value = 42"] --> P1["Gan tham chieu vao bien chia se"]
P1 -. "JMM cho phep reorder 2 buoc tren" .- W1
P1 --> R1["Reader: doc tham chieu"] --> V1["Co the thay value = 0 (default)"]
end
subgraph fin["final field - co freeze action"]
W2["Constructor: ghi final value = 42"] --> F2["Freeze action cuoi constructor"] --> P2["Gan tham chieu"]
P2 --> R2["Reader: doc tham chieu"] --> V2["LUON thay value = 42"]
end
V1 ~~~ W2
style V1 fill:#FCA5A5
style V2 fill:#6EE7B7public final class Money { // khong setter, khong method sua field
private final long amount;
private final String currency;
public Money(long amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
Một Money như thế truyền sang thread khác qua field thường, không volatile, không khóa, mà thread nhận vẫn luôn thấy amount cùng currency đúng. JLS còn bảo đảm thêm: nếu một final field trỏ tới object hoặc mảng khác, reader thấy phiên bản của object/mảng đó ít nhất cũng "mới" ngang với final field. Object mutable không bao giờ có những bảo đảm này.
3. Điều kiện đi kèm: this không escape khỏi constructor
Freeze action nằm ở cuối constructor; thread nào bắt được tham chiếu tới object trước thời điểm đó thì đứng ngoài mọi bảo đảm — có thể thấy final field còn mang giá trị mặc định. Hai cách rò kinh điển, đều trông vô hại:
public final class AuditedMoney {
private final long amount;
public AuditedMoney(long amount, EventBus bus) {
bus.register(this); // RO 1: listener co the duoc goi NGAY
new Thread(this::report).start(); // RO 2: thread moi cam 'this' som
this.amount = amount; // ... khi field con chua duoc ghi
}
}
Đăng ký this vào listener/event bus, hay khởi động thread từ trong constructor và trao this cho nó (kể cả gián tiếp qua method reference hoặc inner class không static, vốn ngầm giữ this) — cả hai đều phát tán một object nửa vời. Quy tắc thực dụng: constructor chỉ gán field; đăng ký và khởi động làm sau khi construct xong, lý tưởng qua static factory method. Đây là điều kiện duy nhất không nhìn thấy qua chữ ký class.
4. Effectively immutable và immutable holder
Nhiều object không khai báo final chỉn chu nhưng thực tế không bao giờ bị sửa sau khi tạo — gọi là effectively immutable: kỹ thuật thì sửa được, theo cách dùng thì không ai sửa. Nó an toàn để chia sẻ miễn là được safe-publish; thiếu đặc quyền của final field, nó cần một cây cầu đồng bộ hóa lúc công bố: volatile field, ConcurrentHashMap, hay BlockingQueue như bài Confinement đã chỉ.
Sức mạnh thật sự của immutability lộ ra khi dữ liệu vừa chia sẻ vừa "thay đổi" theo thời gian. Mẹo nằm ở phân tách: giữ dữ liệu là immutable, để cái thay đổi là tham chiếu trỏ tới nó. "Cập nhật" nghĩa là tạo object mới rồi gán lại tham chiếu — mẫu immutable holder.
public record PriceTable(Map<String, Long> prices) {
public PriceTable { prices = Map.copyOf(prices); } // snapshot bat bien
public long priceOf(String tier) {
Long price = prices.get(tier); // nhan ve Long, kiem null tuong minh
if (price == null) throw new IllegalArgumentException("Unknown tier: " + tier);
return price;
}
public PriceTable withPrice(String tier, long price) { // "wither": tra ve snapshot MOI
Map<String, Long> next = new HashMap<>(prices); // copy ban cu, sua tren ban copy
next.put(tier, price);
return new PriceTable(next);
}
}
public class PriceBoard {
private volatile PriceTable current; // tham chieu doi, doi tuong thi khong
public PriceBoard(PriceTable initial) { this.current = initial; }
public PriceTable snapshot() { return current; } // reader: luon nhat quan
public void publish(PriceTable next) { current = next; } // "cap nhat" = thay nguyen holder
}
// Cap nhat gia VIP: khong sua bang cu, thay nguyen bang moi
board.publish(board.snapshot().withPrice("VIP", 750_000L));
Mỗi PriceTable là một snapshot bất biến của toàn bảng giá; reader gọi snapshot() nhặt một tham chiếu rồi đọc thoải mái, không bao giờ thấy bảng giá nửa cũ nửa mới. Method biến đổi trả về instance mới như withPrice — quen gọi là wither, theo cách đặt tên withX — chẳng có gì để đồng bộ: nó chỉ đọc một object bất biến và tạo object mới. Toàn bộ "sự thay đổi" dồn về đúng một điểm: lệnh gán vào current. Tham chiếu volatile lo phần visibility (snapshot mới hiển thị kịp thời cho reader); tính immutable của PriceTable lo phần nhất quán (không khoảnh khắc nào reader bắt gặp trạng thái dở dang) — công bố trạng thái mới mà không cần một khóa nào.
Nhưng mẫu này có ranh giới cứng: nó chỉ đúng khi việc gán current không phụ thuộc giá trị hiện tại của current, hoặc khi chỉ một thread được phép công bố. Nếu nhiều thread cùng đọc current, cùng dựng phiên bản kế tiếp từ nó, rồi cùng ghi đè, ta rơi vào read-modify-write và một cập nhật bốc hơi — đúng kiểu lost update mà immutability tưởng đã loại bỏ. Đây là chỗ Atomic & CAS nối tiếp: AtomicReference.compareAndSet vá đúng chỗ này.
5. Liên hệ các bài khác
- Immutability — bài mẹ: ba điều kiện của immutable object và defensive copy; bài này mổ bảo đảm bộ nhớ mà điều kiện "mọi field
final" mang lại. - Visibility — dựng nên vấn đề unsafe publication ở mục 1; freeze action là ngoại lệ hiếm cho phép bỏ qua safe publication.
- Confinement — cây cầu
BlockingQueuecủa bài đó là một cách safe-publish cho effectively immutable object. - volatile & synchronized —
volatilelo phần visibility cho immutable holder; bài đó vẽ bản đồ happens-before đầy đủ đứng sau chữ "hiển thị kịp thời". - Atomic & CAS —
AtomicReference.compareAndSetvá đúng chỗ immutable holder vỡ khi nhiều thread cùng publish phiên bản kế tiếp.
6. Deep Dive
Spec / reference chính thức:
- JLS §17.5 — final Field Semantics — đặc tả freeze action và điều kiện "không đọc được tham chiếu trước khi constructor kết thúc".
- JLS §17.5.3 — Subsequent Modification of final Fields — vì sao sửa final field qua reflection phá mọi bảo đảm.
- Java Concurrency in Practice (Goetz et al.), §3.4-3.5 — immutability, safe publication và immutable holder pattern.
Ghi chú: §17.5 là đoạn dễ đọc nhất của chương 17 — ví dụ mở đầu mô tả đúng kịch bản unsafe publication ở mục 1, đáng đọc để thấy spec nói gì bằng ngôn ngữ hình thức.
7. Tóm tắt
- Initialization safety: nếu
thiskhông escape khỏi constructor, mọi thread thấy tham chiếu tới object đều thấyfinalfield đúng giá trị — kể cả publish qua field thường trong data race. - Cơ chế là freeze action (JLS §17.5): freeze xảy ra khi constructor kết thúc; JVM chèn hàng rào bộ nhớ giữa ghi
finalfield và ghi tham chiếu công bố, cấm reorder khiến reader thấy giá trị mặc định. - Điều kiện sống còn:
thiskhông được rò khi constructor còn chạy — đăng ký listener hay khởi động thread trong constructor đều phát tán object nửa vời. Làm sau construct, qua static factory. - Effectively immutable thiếu
finalnên không có freeze action → bắt buộc safe-publish (volatile/lock/concurrent structure). - Immutable holder "cập nhật" bằng thay nguyên snapshot sau tham chiếu
volatile; vỡ khi nhiều thread cùng dựng phiên bản kế tiếp từ giá trị hiện tại (cần CAS).
8. Tự kiểm tra
Q1final field cho guarantee gì khi object được publish qua một data race (field thường, không volatile, không lock)? Cơ chế nào đứng sau?▸
Q2Điều gì xảy ra khi this escape khỏi constructor? Vì sao đăng ký listener hay khởi động thread bên trong constructor là nguy hiểm?▸
Q3Trong PriceBoard, vì sao reader không bao giờ thấy một bảng giá nửa cũ nửa mới? Và ranh giới nào khiến mẫu immutable holder vỡ?▸
Q4Freeze action nằm ở CUỐI constructor, không phải sau mỗi lần gán final field. Vì sao đặt hàng rào bộ nhớ ở đúng chỗ đó là đủ?▸
Q5Effectively immutable khác immutable thật ở điểm nào về yêu cầu khi publish object cho thread khác?▸
Bài tiếp theo: volatile & synchronized — hai cơ chế đồng bộ nội tại của Java
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