Java Internals & Concurrency/Immutability: Thread safety bằng cách không thay đổi
8/47
Bài 8 / 47~20 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: điều kiện immutable, initialization safety của final, cạm bẫy field mutable trong record, và immutable holder.

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. String pool và Integer cache chia sẻ được toàn JVM chính nhờ bất biến, còn record cho cú pháp gọn nhưng không miễn trừ defensive copy.

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.

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 đó ở mức memory model? final.

3. final và initialization safety

final không chỉ 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 — ngoại lệ hiếm hoi của quy tắc safe publication.

3.1 Unsafe publication — khi reader thấy object dở dang

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 field bên trong còn giá trị mặc định (0, null), 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.

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);   // 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 — 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. 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.

3.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: cuối constructor, mọi final field được "đóng băng" — 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
    style V1 fill:#FCA5A5
    style V2 fill:#6EE7B7
public 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. Object mutable không bao giờ có điều đó.

3.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. 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 ở mục trước.

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 ở mục 1, nay đội lốt cú pháp gọn đến mức dễ ru ngủ. Component kiểu mutable như List, Map, hay 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");          // 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.

5. 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);                       // tier la -> null: KHONG unbox mu quang
        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ột bẫy nhỏ trong priceOf: 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 sang long trên null ném NullPointerException ngay tại dấu return, 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 null tường minh (hoặc getOrDefault) đổ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ả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, và chính nó đặt nền cho bài sau: 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 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 tăng dần theo từng booking nên vốn dĩ mutable: mỗi lần đặt vé là đọc số hiện tại, kiểm còn chỗ, rồi ghi tăng — compound action trói sold với capacity của Event. Đổi sang ConcurrentHashMap cũng không cứu: từng thao tác trên map thành thread-safe, nhưng cụm check-then-act vẫn không nguyên tử, invariant "không vượt capacity" vẫn bắc cầu qua nhiều thao tác rời rạc.

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

  • Thread Safety — nơi gọi tên hai vấn đề mà immutability tước mất điều kiện phát sinh, và điểm mặt bốn chiến lược; AtomicityVisibility mổ từng vấn đề.
  • Confinement — chiến lược thứ nhất, cắt tính "shared"; cây cầu BlockingQueue của bài đó chính là một cách safe-publish cho effectively immutable object.
  • volatile & synchronizedvolatile lo 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 & CASAtomicReference.compareAndSet vá đú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 5: vì sao ConcurrentHashMap không cứu được check-then-act của BookingService.

7. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

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 3.1, đáng đọc để thấy spec nói gì bằng ngôn ngữ hình thức.

8. 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 cho không initialization safety (freeze action, JLS §17.5) — object construct đúng cách hiển thị đầy đủ cho mọi thread qua cả một field thường.
  • String pool và Integer cache chia sẻ được toàn JVM chính vì bất biến; record vẫn cần defensive copy khi component là kiểu mutable.
  • Immutable holder "cập nhật" bằng thay nguyên snapshot sau một tham chiếu volatile; nó vỡ khi nhiều thread cùng dựng phiên bản kế tiếp từ giá trị hiện tại.

Nhưng số vé đã bán của một sự kiện vừa shared vừa mutable: không thể giam, không thể đóng băng. Khi cả hai con dao đầu đều cùn, chỉ còn một đường — canh gác mọi truy cập bằng synchronization, chủ đề bài tiếp theo: volatilesynchronized.

9. Tự kiểm tra

Tự kiểm tra
Q1
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?

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.

Q2
final 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?
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 với giá trị đã gán trong constructor, không cần một chút đồng bộ hóa nào. Cơ chế là freeze action của JLS §17.5: cuối constructor, các final field được đóng băng — JVM chèn hàng rào bộ nhớ giữa các lần ghi final field và lần ghi tham chiếu công bố object, cấm kiểu reorder khiến reader bắt được tham chiếu trước khi field được ghi. Field thường không có đặc quyền này: reader qua data race có thể thấy 0 hoặc null.
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?
Vì String immutable tuyệt đối: không method nào sửa nội dung, mọi phép biến đổi đều trả về instance mới, và các field bên trong là final nên hưởng initialization safety — thread nào nhìn thấy tham chiếu cũng thấy nội dung hoàn chỉnh. Dữ liệu không bao giờ đổi thì đọc đồng thời từ bao nhiêu thread cũng cho cùng kết quả, không có cập nhật để mất, không có trạng thái dở dang để bắt gặp. Nếu String mutable, một chỗ sửa literal là mọi nơi dùng chung đổi theo, và mọi chỗ dùng String làm map key, tên class, đường dẫn file đều thành cửa sổ check-then-act.
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?
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ủa final field — nó có thể quan sát object ở trạng thái nửa vời, final field còn mang giá trị mặc định 0/null. Đăng ký this vào event bus nghĩa là listener có thể được gọi ngay lập tức từ thread khác, trước khi các dòng gán phía dưới constructor chạy xong; khởi động thread và trao this cho nó (kể cả ngầm qua method reference hay inner class không static) cũng vậy. 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.
Q5
Trong 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ỡ?
Hai mảnh ghép chia nhau hai vấn đề. Tính immutable của PriceTable lo phần nhất quán: snapshot mà reader cầm không bao giờ đổi ruột dưới chân nó, nên hoặc thấy nguyên bảng cũ, hoặc nguyên bảng mới — không có trạng thái lai. Tham chiếu volatile lo phần visibility: snapshot mới được publish thì hiển thị kịp thời với mọi reader. Mẫu này vỡ khi việc gán current phụ thuộc giá trị hiện tại của current và có nhiều writer: nhiều thread cùng đọc snapshot, cùng dựng phiên bản kế tiếp, cùng ghi đè — read-modify-write quay lại và một cập nhật bốc hơi. Nó chỉ đúng khi một thread duy nhất publish, hoặc cập nhật không phụ thuộc giá trị cũ.
Q6
Phiê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?
Map.get trả về Long (kiểu boxed), còn method khai báo trả về long (kiểu nguyên thủy), nên compiler chèn ngầm một phép auto-unboxing — thực chất là gọi longValue() trên kết quả. Khi tier không tồn tại, get trả về null, và lời gọi longValue() ngầm trên null ném NPE ngay tại dấu return. Lỗi khó truy vì dòng code không có dereference nào nhìn thấy được. Phòng bằng cách 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ý — đừng unbox kết quả Map.get khi key có thể vắng mặt.
Q7
Effectively immutable khác immutable thật ở điểm nào về yêu cầu khi publish object cho thread khác?
Effectively immutable là object về kỹ thuật sửa được (field không final, có thể có setter) nhưng theo cách dùng thì không ai sửa sau khi tạo. Nó an toàn để chia sẻ, nhưng vì thiếu final field nên không có freeze action — không được hưởng initialization safety. Do đó nó bắt buộc phải được safe-publish qua một cây cầu đồng bộ hóa: gán vào field volatile, đặt vào ConcurrentHashMap hay BlockingQueue, hoặc trao tay trong khối khóa. Object immutable thật với mọi field final thì publish qua cả một field thường giữa data race vẫn hiển thị đúng — đó là khác biệt cốt lõi.

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

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