Họ đang đo gì
Bạn có phân biệt được hiển thị và nguyên tử không — hai khái niệm rất hay bị gộp làm một.
Trả lời ngắn~30 giây
volatile bảo đảm mọi luồng đọc được giá trị mới nhất và cấm trình biên dịch/CPU sắp xếp lại quanh nó, nhưng KHÔNG làm thao tác trở nên nguyên tử: count++ trên biến volatile vẫn là đọc–tăng–ghi và vẫn mất cập nhật. synchronized cho cả hai: loại trừ lẫn nhau và hiển thị. volatile đủ khi bạn chỉ GHI ĐÈ chứ không đọc-rồi-tính — kinh điển là một cờ boolean running để dừng vòng lặp.
Giải thích sâu
Vấn đề hiển thị nghe trừu tượng cho tới khi bạn gặp nó: một luồng đặt running = false, luồng kia lặp while (running) và chạy mãi. Không phải vì luồng kia “chưa thấy” theo nghĩa thời gian, mà vì JIT được phép nâng phép đọc ra ngoài vòng lặp — nó thấy biến không đổi trong vòng lặp nên đọc một lần rồi thôi. volatile cấm điều đó. Đây cũng là lý do bug loại này chỉ xuất hiện sau vài giây chạy, khi JIT đã biên dịch xong.
Với bộ đếm, câu trả lời hiện đại không phải synchronized mà là AtomicInteger hoặc LongAdder. AtomicInteger dùng CAS nên không có khoá, nhanh hơn hẳn khi tranh chấp vừa phải; LongAdder chia đếm ra nhiều ô rồi cộng lại khi đọc, nên thắng rõ khi tranh chấp cao và bạn ghi nhiều hơn đọc. Chọn được đúng cái nào cho thấy bạn đã nghĩ tới mức tranh chấp chứ không chỉ tới tính đúng.
Ở mức sâu hơn, cả hai đều được định nghĩa bởi quan hệ happens-before trong Java Memory Model: một lần ghi volatile happens-before mọi lần đọc volatile sau đó của cùng biến, và mở khoá happens-before lần khoá kế tiếp trên cùng monitor. Nói được bằng ngôn ngữ đó thay vì bằng “đẩy giá trị từ cache lên RAM” là khác biệt giữa mô hình đúng và một phép ẩn dụ đã lỗi thời.
// volatile ĐỦ: chỉ ghi đè, không đọc-rồi-tính
private volatile boolean running = true;
public void stop() { running = false; }
public void run() { while (running) { /* … */ } }
// volatile KHÔNG ĐỦ: đọc-tăng-ghi là ba thao tác
private volatile int count;
public void inc() { count++; } // vẫn mất cập nhật
// Đúng, và không khoá
private final AtomicInteger count = new AtomicInteger();
public void inc() { count.incrementAndGet(); }Câu hỏi tiếp theo họ sẽ hỏi
?Double-checked locking có cần volatile không?
Có, và thiếu nó là bug kinh điển: nếu không có volatile, một luồng khác có thể thấy tham chiếu đã khác null trong khi hàm khởi tạo chưa chạy xong, vì việc cấp phát và gán có thể bị sắp xếp lại. Trong thực tế, holder idiom hoặc enum là cách viết singleton gọn hơn và không có bẫy này.
?synchronized khoá lên cái gì?
Phương thức instance khoá trên this, phương thức static khoá trên object Class. Đó là lý do khoá trên this ở lớp công khai là rủi ro: mã bên ngoài cũng khoá được lên chính object đó và bạn có deadlock ngoài tầm kiểm soát. Dùng một object khoá riêng tư là an toàn hơn.
Trả lời thế này là mất điểm
- Nói
volatilelàm thao tác trở nên nguyên tử. Đây là hiểu nhầm số một về từ khoá này. - Giải thích bằng “ghi thẳng vào RAM thay vì cache”. Mô hình đó sai với CPU hiện đại; JMM nói về happens-before.