Java Internals & Concurrency/Immutability: Thread safety bằng cách không thay đổi
11/75
Bài 11 / 75~12 phútConcurrency cơ bảnMiễn phí lượt xem

Immutability: Thread safety bằng cách không thay đổi

Thread safety bằng triệt tính mutable: ba điều kiện để một object bất biến, vì sao String và Integer chia sẻ toàn JVM, và defensive copy khi record ôm field mutable.

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ả — cả atomicity lẫn visibility đều mất điều kiện phát sinh, nên object bất biến an toàn với mọi thread mà không cần một khóa nào. Một object là immutable khi cùng lúc thoả ba điều kiện: 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. String pool và Integer cache chia sẻ được 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 khi component là kiểu mutable như List hay mảng. Bảo đảm bộ nhớ mà final cho không — initialization safety — nằm ở bài 07b.

1. Vì sao một object không bao giờ đổi thì an toàn với mọi thread?

Bài trước khép lại bằng một thừa nhận: confinement chỉ đủ khi ta thật sự có thể không chia sẻ một dữ liệu. Nhưng có những dữ liệu 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. Với chúng, con dao thứ nhất cùn; vẫn còn con dao thứ hai, cắt vào tính từ còn lại.

Gốc của mọi rắc rối nằm ở hai tính từ "shared" và "mutable". Confinement triệt cái thứ nhất; immutability triệt cái thứ hai. Dữ liệu chia sẻ mà không bao giờ đổi thì đọc 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 thread nào bắt gặp thread khác đang sửa giữa chừng. Atomicity và visibility — hai vấn đề nền tảng của bài Thread Safety — 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. Đây là chiến lược thứ hai trong bốn, và phần thưởng không kèm cái giá thường trực của khóa.

Nhưng Java không có từ khóa nào để đóng băng một object; tính immutable dựng nên từ ba ràng buộc phải đồng thời đúng. Thứ nhất, mọi field khai báo final, không gì sửa state sau constructor. Thứ hai, this không escape khi constructor còn chạy, vì thread bắt được tham chiếu tới một object nửa vời có thể thấy nó chưa hoàn chỉnh. Thứ ba, dễ quên nhất: nếu object giữ tham chiếu tới object mutable khác, không thread nào được sửa object ấy, và nó cũng không được rò tham chiếu đó ra ngoài.

💡 Cách nhớ

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à "object không đổi": final chỉ bảo đảm tham chiếu không trỏ đi đâu khác, không nói gì về việc object đượ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;      // RO: giu nguyen tham chieu cua caller
    }
    public List<String> tags() {
        return tags;           // RO: trao thang tham chieu noi bo ra ngoai
    }
}

Caller vẫn giữ tham chiếu tới đúng cái List đã truyền vào và add được bất cứ lúc nào; người gọi tags() cũng nhận về tham chiếu sống. final khóa mũi tên, không khóa thứ ở đầu mũi tên — mọi đường ra-vào của trạng thái mutable đều phải bị bịt.

final khoá tham chiếu, còn ArrayList ở đầu mũi tên vẫn sửa được qua hai lối

final bảo đảm phần bên trái (mũi tên) bất biến, nhưng phần bên phải (object thật) vẫn sửa được nếu tham chiếu tới nó rò ra ngoài. (Điều kiện thứ nhất — final — còn kèm một bảo đảm bộ nhớ mà bài 07b mổ riêng.)

2. String và Integer — immutable quen thuộc quanh ta

Bạn đã hưởng lợi từ immutable object từ ngày đầu học Java. String là ví dụ nổi tiếng nhất: không method nào sửa nội dung — toUpperCase, substring, concat đều trả về một instance mới, 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 — hàng trăm class cùng viết "OK" — được JVM intern thành đúng một instance dùng chung, giữa mọi thread, không một khóa nào. 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, tên class, đường dẫn file đều thành lỗ hổng check-then-act của bài Thread Safety. Cũng nhờ giá trị không đổi mà String dám cache hashCode: 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 giá trị từ -128 đến 127. Hàng nghìn thread cùng cầm 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. Điều gì cho chúng đặc quyền chia sẻ đó ở mức memory model? Chính là final và bảo đảm initialization safety nó mang theo — cơ chế freeze action, mổ ở bài 07b.

3. 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. Từ Java 16, record gói trọn khuôn đó vào một dòng: component final, một canonical constructor, các accessor, và equals/hashCode/toString dựa trên giá trị.

public record Seat(String section, int row, int number) { }

Vỏn vẹn dòng đó cho ta một object bất biến, so sánh theo giá trị, an toàn để hàng nghìn thread cùng đọc — và vì component là final, nó thừa hưởng nguyên initialization safety (bài 07b).

Nhưng record chỉ đóng băng tham chiếu component, không đóng băng thứ chúng trỏ tới — đúng cái bẫy ở điều kiện thứ ba (mục 1), nay đội lốt cú pháp gọn đến mức dễ ru ngủ.

💡 Thử đoán

Một record EventInfo(String id, List<String> tags) được tạo từ một ArrayList do caller truyền vào. Ngoài việc caller vẫn cầm tham chiếu tới list gốc, còn đường nào khác để sửa được ruột info sau khi nó đã tạo xong? Viết ra trước khi đọc tiếp.

Component kiểu mutable như List, Map, hay mảng 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");          // sua tu ben ngoai -- info.tags() da doi
info.tags().add("hacked");      // sua qua accessor -- cung mot List song

info trông như immutable, nhưng info.tags() trả về đúng cái ArrayList caller vẫn cầm. Chỉ một component mutable là cả record mất tính bất biến. Lời giải: compact constructor defensive copy lúc nhận, accessor trả 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 luc nhan: cat day voi List cua caller
    }                               // copyOf bat bien -> accessor mac dinh tra ra cung an toan
}

List.copyOf (Java 10+) tạo bản sao bất biến, cắt đứt liên hệ với List caller truyền vào, đồng thời khiến accessor trả ra thứ không ai add được. Với mảng thì không có bản bất biến sẵn, phải clone cả lúc nhận lẫn lúc trả, hoặc đừng phơi mảng ra ngoài. Nguyên tắc không đổi: 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.

4. Liên hệ các bài khác

  • Thread Safety — nơi gọi tên hai vấn đề (atomicity, visibility) mà immutability tước mất điều kiện phát sinh, và điểm mặt bốn chiến lược.
  • Confinement — chiến lược thứ nhất, cắt tính "shared"; immutability là chiến lược thứ hai, cắt tính "mutable".
  • Initialization safety của final field — vì sao final đủ để một object dựng đúng cách hiển thị an toàn cho mọi thread (freeze action, JLS §17.5), và mẫu immutable holder khi bạn chỉ có "effectively immutable".

5. Deep Dive

📚 Deep Dive — nguồn tham khảo
  • JLS §8.10 — Record Classes — đặc tả record: component final, canonical/compact constructor, accessor tự sinh.
  • Java Concurrency in Practice (Goetz et al.), §3.4 — Immutability: ba điều kiện của immutable object và vì sao immutable luôn thread-safe.
  • List.copyOf — java.util.List — bản sao bất biến, nền của defensive copy hai chiều.

Ghi chú: cơ chế bộ nhớ đứng sau initialization safety (freeze action, JLS §17.5) nằm ở bài 07b — bài này chỉ dùng kết quả của nó ("component final an toàn để chia sẻ").

6. Tóm tắt

  • Immutability tước mất tính "mutable": dữ liệu không bao giờ đổi thì an toàn với mọi thread, không cần đồng bộ hóa.
  • Ba điều kiện phải cùng đúng: mọi field final; this không escape khi construct; không rò tham chiếu tới trạng thái mutable bên trong.
  • final khóa tham chiếu, không khóa object ở đầu tham chiếu — điều kiện thứ ba (defensive copy hai chiều) là ranh giới giữa "field không đổi" và "object không đổi".
  • String pool và Integer cache chia sẻ được toàn JVM chính vì bất biến; giá trị không đổi còn cho phép String cache hashCode.
  • record cho object bất biến trong một dòng, nhưng vẫn cần defensive copy khi component là kiểu mutable — copy lúc nhận, trả bản không sửa được lúc xuất.

7. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Ba điều kiện nào phải cùng đúng để một object là immutable? Vì sao thiếu điều kiện thứ ba là dễ mắc nhất?
  2. Q2
    Vì 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?
  3. Q3
    Vì 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?
  4. Q4
    Vì sao nói immutability không 'giải' atomicity và visibility mà 'tước mất điều kiện phát sinh' của chúng?
  5. Q5
    Integer a = 127, b = 127 cho a == b là true, nhưng với 128 lại false. Cơ chế nào đứng sau, và nó liên quan gì tới immutability?

Bài tiếp theo: Initialization safety của final field — freeze action và immutable holder

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

Đặt 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

Bài tiếp theo

Initialization safety của final field — freeze action