Immutability: Thread safety bằng cách không thay đổi
Thread safety bằng cách triệt tính mutable: điều kiện để một object là immutable, final và initialization safety, record cùng cạm bẫy field mutable, và immutable holder để cập nhật bằng cách thay thế.
TL;DR: Immutability triệt tiêu tính "mutable": dữ liệu không bao giờ đổi sau khi construct thì đọc lúc nào cũng ra cùng kết quả — atomicity lẫn visibility đều mất điều kiện phát sinh. Ba điều kiện phải cùng đúng: mọi field final, this không escape khỏi constructor, và không rò tham chiếu tới trạng thái mutable bên trong. final không chỉ cấm gán lại — JLS §17.5 cho nó một "freeze action" cuối constructor, nên object construct đúng cách publish qua field thường vẫn hiển thị đầy đủ cho mọi thread; field thường không có đặc quyền đó (reader có thể thấy 0/null). String và Integer cache được chia sẻ toàn JVM chính nhờ bất biến. record cho cú pháp gọn nhưng không miễn trừ defensive copy. "Cập nhật" dữ liệu chia sẻ = thay nguyên snapshot sau một tham chiếu volatile.
1. Giới thiệu
Bài trước khép lại bằng một thừa nhận: confinement chỉ là đủ khi ta thật sự có thể không chia sẻ một dữ liệu. Nhưng có những dữ liệu, theo bản chất bài toán, buộc phải đến tay nhiều thread — một Event mà hàng nghìn request cùng đọc, một bảng giá dùng chung cho cả hệ thống. Với chúng, con dao thứ nhất cùn: ta không thể cắt bỏ tính "shared". Vẫn còn con dao thứ hai, và nó cắt vào tính từ còn lại.
Gốc của mọi rắc rối luôn nằm ở hai tính từ "shared" và "mutable". Confinement triệt cái thứ nhất; immutability triệt cái thứ hai. Nếu một dữ liệu được chia sẻ nhưng không bao giờ thay đổi sau khi khởi tạo, thì đọc nó lúc nào cũng cho cùng một kết quả. Không có cập nhật thì không có lost update; không có trạng thái dở dang thì không có chuyện một thread bắt gặp một thread khác đang sửa giữa chừng. Atomicity và visibility, cả hai vấn đề nền tảng của bài thread safety, đều bốc hơi cùng lúc — không phải vì ta giải được chúng, mà vì ta tước mất điều kiện để chúng phát sinh. Một đối tượng immutable an toàn với bất kỳ số lượng thread nào, mãi mãi, mà không cần một dòng đồng bộ hóa nào.
Đây là chiến lược thứ hai trong bốn chiến lược, và nó đẹp ở chỗ phần thưởng không kèm cái giá thường trực của khóa. Nhưng "không bao giờ thay đổi" là một lời hứa khó giữ hơn vẻ ngoài. Java không có một từ khóa duy nhất để đóng băng một đối tượng; tính immutable được dựng nên từ vài ràng buộc phải đồng thời đúng, và chỉ cần một field rò ra một tham chiếu mutable là cả lâu đài sụp. Bài này đi từ định nghĩa cho chặt thế nào là immutable, tới bảo đảm mà ngôn ngữ cho không qua final, tới record như carrier immutable tự nhiên cùng cạm bẫy của nó, và cuối cùng tới cách "cập nhật" một dữ liệu mà không hề sửa nó.
2. Thế nào là một immutable object
Một đối tượng là immutable khi trạng thái của nó không thể thay đổi sau khi construct xong. Nghe đơn giản, nhưng để bảo đảm điều đó cứng cáp, cần ba điều kiện cùng đúng, và bỏ sót bất kỳ điều nào là đủ để phá vỡ.
Thứ nhất, mọi field phải được khai báo final, và state của đối tượng không được thay đổi sau khi constructor chạy xong. Không có setter, không có method nào sửa field. Thứ hai, đối tượng phải được khởi tạo trọn vẹn trong constructor — không để this escape ra ngoài khi construct còn dở dang, vì một thread khác bắt được tham chiếu tới một đối tượng nửa vời có thể thấy nó ở trạng thái chưa hoàn chỉnh. Thứ ba, và đây là điều dễ quên nhất: nếu đối tượng giữ tham chiếu tới một đối tượng mutable khác, không thread nào được phép sửa đối tượng mutable ấy, và bản thân nó cũng không được rò tham chiếu đó ra ngoài.
Ba điều kiện của immutable object: (1) mọi field final, không gì sửa state sau constructor; (2) this không escape khi construct còn dở; (3) trạng thái mutable bên trong không rò ra ngoài — defensive copy lúc nhận, bản không sửa được lúc trả.
Điều kiện thứ ba là ranh giới tinh tế giữa "field không đổi" và "đối tượng không đổi". Một field final chỉ bảo đảm cái tham chiếu không trỏ đi đâu khác; nó không nói gì về việc đối tượng được trỏ tới có đổi ruột hay không.
public final class EventTags {
private final List<String> tags;
public EventTags(List<String> tags) {
this.tags = tags; // RÒ: giữ nguyên tham chiếu của caller
}
public List<String> tags() {
return tags; // RÒ: trao thẳng tham chiếu nội bộ ra ngoài
}
}
tags là final, class có vẻ immutable, nhưng nó không hề. Caller vẫn giữ tham chiếu tới đúng cái List đã truyền vào và có thể add thêm phần tử bất cứ lúc nào; người gọi tags() cũng nhận về tham chiếu sống và sửa được. Cái final chỉ khóa mũi tên, không khóa thứ ở đầu mũi tên. Một đối tượng chỉ thật sự immutable khi mọi đường ra-vào của trạng thái mutable đều bị bịt — bằng defensive copy lúc nhận, và bằng bản sao hoặc view không sửa được lúc trả.
3. String và Integer — immutable quen thuộc quanh ta
Trước khi tự dựng immutable object, hãy nhận ra bạn đã hưởng lợi từ chúng suốt từ ngày đầu học Java. String là immutable object nổi tiếng nhất: không một method nào của nó sửa nội dung — toUpperCase, substring, concat đều trả về một instance mới, còn chuỗi gốc giữ nguyên vĩnh viễn.
String s = "olhub";
String upper = s.toUpperCase(); // tao instance MOI -- "olhub" khong he doi
System.out.println(s); // van in "olhub"
Integer a = 127, b = 127; // autoboxing goi Integer.valueOf -> dung cache
System.out.println(a == b); // true: CUNG mot object trong cache -128..127
Tính bất biến đó là điều kiện sống còn cho hai cơ chế chia sẻ quy mô toàn JVM. Thứ nhất là String pool: mọi string literal giống nhau trong chương trình — hàng trăm class cùng viết "OK" — được JVM intern thành đúng một instance dùng chung. Chia sẻ một object cho toàn bộ chương trình, giữa mọi thread, mà không một khóa nào — điều này chỉ an toàn vì String không bao giờ đổi ruột. Hãy tưởng tượng String mutable: một chỗ sửa "OK" thành "KO" là mọi nơi dùng literal đó đổi theo, và mọi chỗ dùng String làm key của map, làm tên class, làm đường dẫn file đều thành lỗ hổng — kiểm tra xong, kẻ khác sửa, rồi mới dùng, đúng hình mẫu check-then-act của bài Thread Safety. Cũng nhờ giá trị không đổi mà String dám cache hashCode của nó: tính một lần, dùng mãi.
Thứ hai là Integer cache: Integer.valueOf (mà autoboxing gọi ngầm) trả về object dùng chung từ cache cho các giá trị từ -128 đến 127. Hàng nghìn thread cùng cầm chung một object Integer 127 — an toàn tuyệt đối, vì không ai sửa được ruột nó. Bài học chung: immutable là giấy phép để chia sẻ thoải mái. Câu hỏi tiếp theo là điều gì cho String và Integer đặc quyền đó ở mức memory model — câu trả lời nằm ở final.
4. final và initialization safety
Lý do final đáng được nhấn mạnh không chỉ vì nó cấm gán lại. Nó còn mang một bảo đảm về bộ nhớ mà Java Memory Model cho không, và đây là một ngoại lệ hiếm hoi của quy tắc safe publication.
4.1 Unsafe publication — khi reader thấy object dở dang
Bài Thread Safety đã dựng nên vấn đề: nếu một thread tạo một đối tượng rồi công bố tham chiếu của nó cho thread khác mà không có đồng bộ hóa nào, thread thứ hai có thể thấy tham chiếu đã được gán nhưng lại thấy các field bên trong còn ở giá trị mặc định — 0, null — vì các thao tác ghi field và thao tác công bố tham chiếu có thể bị reorder. Đây chính là lý do một đối tượng mutable cần được safe-publish, bằng volatile, bằng khóa, hay bằng một cấu trúc concurrent.
Viết hẳn thành chương trình chạy được để thấy hình dạng của lỗi:
public class UnsafePublication {
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) {
new Thread(() -> {
while (holder == null) { } // doi tham chieu xuat hien
System.out.println(holder.value); // JMM cho phep in 0 thay vi 42
}).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 — và hai bước sau được phép hoán đổi thứ tự khi không có đồng bộ hóa. Reader vì thế có thể bắt được tham chiếu trước khi value được ghi, và in ra 0. Trên x86 với JVM hiện nay bạn sẽ chạy cả nghìn lần không tái hiện được — chính sự "gần như luôn đúng" đó khiến lỗi này hiểm: nó hợp lệ theo spec, và sẽ chọn đúng kiến trúc CPU yếu về ordering hoặc đúng lần JIT tối ưu mạnh tay để xuất hiện.
4.2 Freeze action — đặc quyền JLS §17.5 của final field
JMM trao cho final field một đặc quyền, đặc tả ở JLS §17.5 (final field semantics): nếu một đối tượng được construct đúng cách — this không escape khi constructor còn chạy — thì bất kỳ thread nào nhìn thấy tham chiếu tới đối tượng đó đều được bảo đảm thấy các final field của nó với giá trị đã gán trong constructor, mà không cần một chút đồng bộ hóa nào. Cơ chế đứng sau có tên: freeze action. Tại thời điểm constructor kết thúc, mọi final field được "đóng băng" — về mặt cài đặt, 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ố đối tượng, cấm đúng kiểu hoán đổi thứ tự đã làm UnsafePublication in ra 0. Reader đi tới object qua tham chiếu đọc được sau freeze thì chắc chắn thấy giá trị đã gán.
So sánh hai con đường publish:
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"]
R1 --> 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"]
F2 --> P2["Gan tham chieu vao bien chia se"]
P2 --> R2["Reader: doc tham chieu"]
R2 --> V2["LUON thay value = 42"]
endpublic final class Money {
private final long amount;
private final String currency;
public Money(long amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// ... không setter, không method sửa field
}
Một Money như thế có thể được tạo ở một thread rồi truyền sang thread khác qua một field thường, không volatile, không khóa, và thread nhận vẫn luôn thấy amount cùng currency đúng. Đây là điều mà một đối tượng mutable không bao giờ có.
4.3 Điều kiện đi kèm: this không escape khỏi constructor
Đặc quyền freeze gắn chặt vào việc construct cho đúng: this không được rò ra ngoài khi constructor còn chạy. Freeze action nằm ở cuối constructor; nếu một thread khác bắt được tham chiếu tới object trước thời điểm đó, nó đứng ngoài mọi bảo đảm — có thể thấy final field còn mang giá trị mặc định, vì với nó object chưa hề được "đóng băng".
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: listener co the duoc goi NGAY,
// truoc khi dong duoi gan xong
this.amount = amount;
}
}
public final class PollingPrice {
private final long amount;
public PollingPrice(long amount) {
new Thread(this::report).start(); // RO: thread moi cam 'this'
this.amount = amount; // ... khi field con chua duoc ghi
}
}
Đăng ký this vào một listener/event bus, hay khởi động một thread từ bên 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ữ tham chiếu tới 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; mọi việc đăng ký, khởi động, phát tán làm sau khi construct xong, lý tưởng là qua một static factory method tạo object xong xuôi rồi mới register/start. Đây chính là điều kiện thứ hai trong ba điều kiện ở mục 2 — và là điều kiện duy nhất không nhìn thấy được qua chữ ký class, chỉ thấy được khi đọc thân constructor.
5. record — carrier immutable tự nhiên
Viết tay một immutable class đúng kiểu khá lắm lời: field final, constructor gán hết, một loạt getter, rồi equals, hashCode, toString cho khớp. Từ Java 16, record đóng gói trọn cái khuôn đó vào một dòng. Một record sinh ra các component là final, một canonical constructor gán chúng, các accessor, và equals/hashCode/toString dựa trên giá trị — đúng bộ xương của một value object immutable.
public record Seat(String section, int row, int number) { }
Vỏn vẹn dòng đó cho ta một đối tượng bất biến, so sánh theo giá trị, an toàn để hàng nghìn thread cùng đọc. Vì các component là final, nó cũng thừa hưởng nguyên initialization safety ở mục trước: một Seat công bố qua field thường vẫn hiển thị đầy đủ và đúng cho mọi thread.
Nhưng record chỉ đóng băng các tham chiếu component, không đóng băng thứ chúng trỏ tới — đúng cái bẫy ở mục 2, nay đội lốt một cú pháp gọn gàng đến mức dễ ru ngủ. Một component kiểu mutable như List, Map, hay một mảng vẫn rò trạng thái sửa được ra cả hai đầu:
public record EventInfo(String id, List<String> tags) { }
var tags = new ArrayList<>(List.of("music", "vip"));
var info = new EventInfo("concert-01", tags);
tags.add("cancelled"); // sửa từ bên ngoài — info.tags() đã đổi
info.tags().add("hacked"); // sửa qua accessor — cùng một List sống
info trông như immutable, nhưng info.tags() trả về đúng cái ArrayList mà caller vẫn cầm, và accessor mặc định cũng trao thẳng tham chiếu nội bộ. Chỉ một component mutable là cả record mất tính bất biến.
Lời giải là một compact constructor để defensive copy lúc nhận, và một accessor trả ra bản không sửa được lúc xuất:
public record EventInfo(String id, List<String> tags) {
public EventInfo {
tags = List.copyOf(tags); // copy lúc nhận: cắt dây với List của caller
}
// List.copyOf đã cho ra một List bất biến, nên accessor mặc định trả nó cũng an toàn
}
List.copyOf (Java 10+) tạo một bản sao bất biến, cắt đứt liên hệ với List mà caller truyền vào và đồng thời khiến accessor trả ra một thứ không ai add được. Với mảng thì không có bản bất biến sẵn, nên phải clone cả lúc nhận lẫn lúc trả, hoặc tốt hơn là đừng phơi mảng ra ngoài. Nguyên tắc không đổi so với một immutable class viết tay: mọi trạng thái mutable phải bị bịt ở cả lối vào lẫn lối ra; record cho ta cú pháp, không cho ta miễn trừ trách nhiệm ấy.
6. Effectively immutable và immutable holder
Không phải đối tượng nào cũng được khai báo final chỉn chu, nhưng nhiều đối tượng trên thực tế không bao giờ bị sửa sau khi tạo. Một đối tượng như thế gọi là effectively immutable: kỹ thuật thì nó sửa được, nhưng theo cách dùng thì không ai sửa. Một effectively immutable object an toàn để chia sẻ giữa nhiều thread miễn là nó được safe-publish — vì 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ố, chẳng hạn đặt nó vào một volatile field, một ConcurrentHashMap, hay một BlockingQueue như bài Confinement đã chỉ.
Sức mạnh thật sự của immutability lộ ra khi ta cần một dữ liệu vừa chia sẻ vừa "thay đổi" theo thời gian. Nghe như mâu thuẫn với cả bài, nhưng mẹo nằm ở chỗ phân tách: giữ bản thân dữ liệu là immutable, và để cái thay đổi là tham chiếu trỏ tới nó. "Cập nhật" không còn nghĩa là sửa đối tượng tại chỗ, mà là tạo nguyên một đối tượng mới mang trạng thái mới rồi gán lại tham chiếu. Đây là 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); // tier la -> null, KHONG duoc unbox mu quang
if (price == null) {
throw new IllegalArgumentException("Unknown tier: " + tier);
}
return price;
}
}
public class PriceBoard {
private volatile PriceTable current; // tham chiếu thay đổi, đối tượng thì không
public PriceBoard(PriceTable initial) { this.current = initial; }
public PriceTable snapshot() { return current; } // reader: luôn nhất quán
public void publish(PriceTable next) { // "cập nhật" = thay nguyên holder
current = next;
}
}
Một bẫy nhỏ trong priceOf đáng một câu chú thích: viết gọn return prices.get(tier); với kiểu trả về long trông vô hại, nhưng Map.get trả về Long — tier không tồn tại thì nhận null, và phép auto-unboxing từ Long sang long trên null ném NullPointerException ngay tại dấu return, một NPE khó truy vì dòng code chẳng có chỗ nào trông như dereference. Nhận về biến Long rồi kiểm tra null tường minh (hoặc getOrDefault khi có giá trị mặc định hợp lý) tốn thêm hai dòng nhưng đổi được thông điệp lỗi nói thẳng vấn đề.
Mỗi PriceTable là một snapshot bất biến của toàn bộ bảng giá. Reader gọi snapshot() nhặt được một tham chiếu và đọc thoải mái, chắc chắn không bao giờ thấy một bảng giá nửa cũ nửa mới, vì cái nó cầm sẽ không đổi ruột dưới chân nó. Người cập nhật dựng một PriceTable mới hoàn chỉnh rồi gán một phát vào current.
Để việc "dựng phiên bản mới từ phiên bản cũ" bớt lắm lời, immutable object thường tự cung cấp các method biến đổi trả về instance mới — quen gọi là wither (theo cách đặt tên withX, họ hàng chức năng với toUpperCase của String):
public record PriceTable(Map<String, Long> prices) {
// ... constructor va priceOf nhu tren ...
public PriceTable withPrice(String tier, long price) {
Map<String, Long> next = new HashMap<>(prices); // copy ban cu
next.put(tier, price); // sua tren ban copy
return new PriceTable(next); // tra ve snapshot MOI
}
}
// Cap nhat gia VIP: khong sua bang cu, thay nguyen bang moi
board.publish(board.snapshot().withPrice("VIP", 750_000L));
Bản thân withPrice không có gì để đồng bộ — nó chỉ đọc một object bất biến và tạo một 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 — bảo đảm snapshot mới hiển thị kịp thời cho reader — còn tính immutable của PriceTable lo phần nhất quán: không có khoảnh khắc nào reader bắt gặp trạng thái dở dang. Hai mảnh ghép đó hợp lại cho ta một cách công bố trạng thái mới mà không cần một khóa nào.
Mẫu này gọn và hấp dẫn, nhưng nó có một ranh giới cứng, và chính ranh giới ấy đặt nền cho bài sau. Nó chỉ đúng khi việc gán current không phụ thuộc vào giá trị hiện tại của current, hoặc khi chỉ một thread duy nhất đượ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 dựa trên nó, rồi cùng ghi đè, ta lại rơi vào read-modify-write và một cập nhật sẽ bốc hơi — đúng kiểu lost update mà immutability tưởng đã loại bỏ.
Đây cũng là lý do immutability một mình không vá được race condition của BookingService trong TicketFlow. Số vé đã bán phải tăng dần theo từng booking, nên nó vốn dĩ mutable theo thời gian; mỗi lần đặt vé là đọc số hiện tại, kiểm còn chỗ, rồi ghi tăng — một compound action trói sold với capacity của Event. Đổi HashMap thành ConcurrentHashMap cũng không cứu được: từng thao tác trên map trở nên thread-safe, nhưng cụm check-then-act vẫn không nguyên tử, và invariant "không vượt capacity" vẫn bắc cầu qua nhiều thao tác rời rạc mà không có gì quây chúng lại thành một khối. Hai thread vẫn có thể cùng đọc thấy còn một chỗ rồi cùng bán.
7. Liên hệ các bài khác
- Thread Safety — định nghĩa atomicity và visibility, hai vấn đề mà immutability tước mất điều kiện phát sinh; bốn chiến lược cũng gọi tên ở đó.
- Confinement — chiến lược thứ nhất, cắt tính "shared"; cây cầu
BlockingQueuecủa bài đó chính 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. - Delegation & concurrent collections — phát triển tiếp nhận xét cuối mục 6: vì sao
ConcurrentHashMapkhông cứu được check-then-act củaBookingService.
8. 📚 Deep Dive Oracle
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à một trong những đoạn dễ đọc nhất của chương 17 — phần ví dụ mở đầu mô tả đúng kịch bản UnsafePublication của bài này, đáng đọc để thấy spec nói gì bằng ngôn ngữ hình thức.
9. Tổng kết
Immutability trả lời câu hỏi thread safety bằng cách tước mất tính "mutable": một dữ liệu không bao giờ đổi thì đọc lúc nào cũng cho cùng kết quả, an toàn với bất kỳ số thread nào mà không cần đồng bộ hóa. Một đối tượng thật sự immutable đòi ba điều cùng đúng — mọi field final, this không escape khi construct, và không rò tham chiếu tới bất kỳ trạng thái mutable nào nó giữ. final không chỉ cấm gán lại; nó còn cho không initialization safety, một ngoại lệ của safe publication khiến đối tượng construct đúng cách hiển thị đầy đủ cho mọi thread qua cả một field thường. record đóng gói khuôn value object immutable vào một dòng, nhưng vẫn để hở đúng cái bẫy cũ khi component là kiểu mutable, nên cần defensive copy ở lối vào và bản không sửa được ở lối ra. Và khi cần một dữ liệu vừa chia sẻ vừa biến thiên, immutable holder cho ta cách "cập nhật" bằng thay nguyên đối tượng sau một tham chiếu volatile, đạt nhất quán mà không tốn một khóa.
Immutability là đủ khi ta có thể dựng dữ liệu một lần rồi không bao giờ sửa, hoặc khi mỗi thay đổi là thay nguyên một snapshot và chỉ một thread công bố. Nhưng có những trạng thái không cho ta cái quyền đó. Số vé đã bán của một sự kiện phải được nhiều thread cùng nhìn thấy và phải tăng dần theo từng booking — nó vừa shared vừa mutable, không thể giam, không thể đóng băng. Khi cả hai con dao đầu tiên đều cùn, khi buộc phải giữ cả "shared" lẫn "mutable" cùng lúc, chỉ còn một đường: canh gác mọi truy cập bằng synchronization. Đó là chủ đề bài tiếp theo, bắt đầu từ hai cơ chế đồng bộ mà Java cài sẵn ngay trong ngôn ngữ — volatile và synchronized.
10. Tự kiểm tra
Q1Vì sao một record chứa List vẫn có thể không immutable, dù mọi component của record đều final? Vá thế nào cho kín cả hai chiều?▸
final trên component chỉ khóa tham chiếu — mũi tên không trỏ đi đâu khác — chứ không khóa object ở đầu mũi tên. Một component kiểu List rò trạng thái sửa được ra cả hai đầu: caller vẫn cầm tham chiếu tới đúng cái list đã truyền vào và add được từ bên ngoài; accessor mặc định lại trao thẳng tham chiếu nội bộ cho người gọi.
Vá bằng compact constructor làm defensive copy lúc nhận — tags = List.copyOf(tags) — cắt dây với list của caller; vì List.copyOf cho ra một list bất biến, accessor mặc định trả nó ra cũng an toàn luôn. Với mảng thì không có bản bất biến sẵn, phải clone cả hai chiều hoặc đừng phơi mảng ra ngoài.
Q2final 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?▸
Q3Vì sao JVM dám cho hàng trăm class cùng chia sẻ đúng một instance String trong String pool, giữa mọi thread, mà không một khóa nào?▸
Q4Đ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?▸
Q5Trong 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ỡ?▸
Q6Phiên bản đầu của priceOf viết gọn return prices.get(tier); với kiểu trả về long. Vì sao dòng này có thể ném NullPointerException dù không có dấu chấm dereference nào?▸
Q7Effectively 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 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