Java Memory Model (JMM): Giải mã Happens-Before, Volatile và CAS tận gốc rễ phần cứng
- Published on
- /7 mins read/
Lập trình đa luồng (Concurrency) trong Java khó không phải vì cú pháp, mà vì thế giới phần cứng bên dưới chạy theo những quy luật hoàn toàn khác với cách chúng ta đọc code tuần tự từ trên xuống dưới.
1. Bí ẩn của đoạn code "chạy mãi không dừng"
Hãy xem đoạn mã Java cực kỳ đơn giản sau:
public class VisibilityDemo {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (!stop) {
count++;
}
System.out.println("Worker kết thúc với count: " + count);
});
worker.start();
Thread.sleep(100);
stop = true; // Main thread đổi biến thành true
System.out.println("Main thread đã set stop = true");
worker.join();
}
}Nếu bạn chạy chương trình này ở chế độ -server (mặc định trên JVM 64-bit), chương trình sẽ không bao giờ kết thúc! Main thread đã in dòng "Main thread đã set stop = true", nhưng luồng worker vẫn tiếp tục quay vòng vô tận trong vòng lặp while (!stop).
Vì sao?
Bản chất phần cứng:
- CPU Caches & Memory Visibility: Mỗi lõi CPU có bộ nhớ đệm riêng siêu nhanh (L1/L2 Cache). Luồng
workerđọc giá trịstopmột lần vào Cache/Register, và JIT Compiler nhận thấy trong thân vòng lặp không có gì thay đổi biếnstop, nó tự động tối ưu thànhwhile (true) count++;(Hoisting). - Khi
mainthread ghistop = true, giá trị này có thể chỉ nằm trong Write Buffer của Core 1 hoặc RAM mà chưa hề được làm tươi (flush/invalidate) vào Cache của Core 2.
Để lập trình viên không phải viết assembly riêng cho từng loại CPU (x86, ARM, RISC-V), Java định nghĩa ra Java Memory Model (JMM).
2. Java Memory Model (JMM) là gì?
JMM (được chuẩn hóa lại trong JSR-133) là một bản đặc tả trừu tượng quy định: Khi nào một hành động ghi (Write) của một luồng được đảm bảo là nhìn thấy (Visible) bởi hành động đọc (Read) của một luồng khác.
JMM tạo ra sự cân bằng hoàn hảo giữa:
- Tự do cho phần cứng & Compiler: Cho phép CPU sắp xếp lại lệnh (Instruction Reordering), dùng cache, vectorization để đạt tốc độ cao nhất.
- Tính khả đoán cho Lập trình viên: Cung cấp các công cụ tường minh (
volatile,synchronized,final,VarHandle) để khóa các giới hạn an toàn.
3. Nguyên tắc vàng: Quan hệ Happens-Before
Nếu hành động A happens-before hành động B, thì mọi kết quả và thay đổi dữ liệu của A đều được đảm bảo nhìn thấy bởi B, và thứ tự thực thi của A luôn đứng trước B.
Các quy tắc Happens-Before cốt lõi mà mọi Senior Java Dev phải nằm lòng:
- Program Order Rule: Trong cùng một luồng duy nhất, mỗi hành động đứng trước xảy ra trước mọi hành động đứng sau theo thứ tự mã nguồn.
- Volatile Variable Rule: Việc ghi (Write) vào một biến
volatileluôn happens-before mọi hành động đọc (Read) tiếp theo của chính biến đó. - Monitor Lock Rule: Việc mở khóa (
unlock) một monitor / blocksynchronizedluôn happens-before mọi hành động khóa (lock) tiếp theo trên cùng monitor đó. - Thread Start Rule: Lời gọi
thread.start()luôn happens-before bất kỳ câu lệnh nào bên trong luồng con đó. - Thread Join Rule: Mọi câu lệnh trong luồng con luôn happens-before thời điểm câu lệnh
thread.join()của luồng cha hoàn thành. - Transitivity (Tính bắc cầu): Nếu
A \prec BvàB \prec C, thì chắc chắnA \prec C.
4. volatile hoạt động ở mức vi xử lý như thế nào?
Khi ta khai báo private static volatile boolean stop = false;, JVM sẽ sinh ra các mã lệnh máy đặc biệt gọi là Memory Barrier (Fence):
Hai bảo chứng của volatile:
- Tính hiển thị (Visibility): Giá trị ghi vào lập tức được flush ra bộ nhớ dùng chung, và các lõi khác bị ép nạp lại giá trị mới nhất.
- Chống Reordering: Trình biên dịch và CPU bị cấm di chuyển các lệnh đọc/ghi xung quanh rào cản
volatile.
Lưu ý cốt tử: volatile KHÔNG đảm bảo tính nguyên tử (Atomicity)! Biến volatile int count; count++ vẫn bị Race Condition vì count++ gồm 3 bước riêng biệt: Read, Modify, Write.
5. Nguyên tử hóa không khóa: Compare-And-Swap (CAS)
Để giải quyết bài toán Atomicity mà không phải trả giá đắt cho việc lock luồng bằng synchronized, các lớp như AtomicInteger, AtomicReference sử dụng lệnh phần cứng CAS (Compare-And-Swap).
Đoạn mã mã máy của lệnh CAS trên chip Intel x86 là lock cmpxchg:
- Lệnh này khóa bus bộ nhớ hoặc cache line tại cấp độ phần cứng chỉ trong vài chu kỳ CPU.
- Nếu giá trị trong RAM vẫn đúng bằng
expected, nó đổi thànhupdate. - Nếu có luồng khác chen ngang đổi giá trị, CAS trả về
false, và Java sẽ lặp lại vòng xoay (Spin lock) cho đến khi thành công.
// Cách AtomicInteger tăng giá trị trong JDK:
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
// Bên dưới sử dụng VarHandle hoặc Unsafe gọi trực tiếp lệnh CPU cmpxchg6. Tổng kết bảng so sánh công cụ Concurrency trong Java
| Cơ chế | Tính hiển thị (Visibility) | Tính nguyên tử (Atomicity) | Khóa luồng (Thread Blocking) | Chi phí hiệu năng |
|---|---|---|---|---|
| Biến thông thường | Không bảo đảm | Không (ngoại trừ 32-bit types) | Không | Siêu nhanh |
volatile | Có (100%) | Không | Không | Rất nhẹ (vài chu kỳ CPU) |
AtomicInteger (CAS) | Có | Có | Không (Lock-Free Spin) | Rất nhẹ khi ít tranh chấp |
ReentrantLock / synchronized | Có | Có | Có (Treo luồng qua OS) | Nặng hơn nếu tranh chấp cao |
Nắm vững Java Memory Model và quan hệ Happens-Before là lằn ranh phân biệt giữa một lập trình viên Java nghiệp dư và một kỹ sư hạ tầng backend thực thụ. Bạn sẽ không còn hoang mang trước những bug "lúc chạy được lúc không" trên môi trường Production đa lõi!
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- 1. Bí ẩn của đoạn code "chạy mãi không dừng"
- Bản chất phần cứng:
- 2. Java Memory Model (JMM) là gì?
- 3. Nguyên tắc vàng: Quan hệ Happens-Before
- 4. volatile hoạt động ở mức vi xử lý như thế nào?
- Hai bảo chứng của volatile:
- 5. Nguyên tử hóa không khóa: Compare-And-Swap (CAS)
- 6. Tổng kết bảng so sánh công cụ Concurrency trong Java