Từ khóa volatile được sử dụng để đánh dấu một biến Java là "đã được lưu trữ trong bộ nhớ chính". Chính xác hơn có nghĩa là, mọi lần đọc biến volatile sẽ được đọc từ bộ nhớ chính của máy tính chứ không phải từ bộ đệm CPU và mọi hành động ghi vào biến volatile sẽ được ghi vào bộ nhớ chính chứ không chỉ ghi vào bộ đệm CPU .

Các vấn đề cần nhìn nhận khi sử dụng một biến có thể thay đổi

Biến volatile đảm bảo khả năng đồng bộ giá trị biến trên các luồng. Ví dụ, trong một ứng dụng đa luồng trong đó các luồng hoạt động sử dụng biến non-volatile. Vì lý do hiệu năng mỗi luồng có thể sao chép các biến từ bộ nhớ chính vào bộ đệm CPU trong khi làm việc với chúng. Nếu máy tính của bạn chứa nhiều CPU, mỗi luồng có thể chạy trên một CPU khác nhau. Điều đó có nghĩa là, mỗi luồng có thể sao chép các biến vào bộ đệm CPU của các CPU khác nhau. Điều này được minh họa ở đây:

Với các biến non-volatile, không có gì đảm bảo khi Máy ảo Java (JVM) đọc dữ liệu từ bộ nhớ chính vào bộ nhớ CPU hoặc ghi dữ liệu từ bộ nhớ CPU vào bộ nhớ chính. Điều này có thể gây ra một số vấn đề mà tôi sẽ giải thích trong các phần sau.
Hãy tưởng tượng một tình huống trong đó hai hoặc nhiều luồng có quyền truy cập vào một đối tượng được chia sẻ có chứa một biến counter được khai báo như sau:

public class SharedObject {
    public int counter = 0;
}

Tưởng tượng rằng, chỉ có thread1 tăng biến counter, nhưng cả thread1 và thread2 đều đọc biến này theo thời gian. Nếu biến counter không được khai báo với volatile thì không có gì đảm bảo khi nào giá trị của biến counter được ghi từ bộ đệm CPU vào bộ nhớ chính. Điều này có nghĩa rằng giá trị của biến counter trong CPU và bộ nhớ chính có thể không giống nhau, dấn đến sự cập nhập giá trị biến của một thread không được các thread khác biết đến. Đây được gọi là vấn đề "khả năng hiển thị". Tình huống được minh họa trong hình bên dưới đây:

Volatile đảm bảo khả năng cho thấy sự thay đổi giữa các thread (khả năng hiển thị)

Volatile được sinh ra nhằm giải quyết các vấn đề về khả năng thấy sự thay đổi giữa các thread. Bằng cách khai báo biến counter trong ví dụ trên với từ khóa volatile, sự thay đổi biến counter trong thread1 sẽ ngay lập tức được ghi vào bộ nhớ chính. Ngoài ra, tất cả các hành động đọc biến counter sẽ được đọc từ bộ nhớ chính. Đây là cách khai báo biến volatile:

public class SharedObject {
    public volatile int counter = 0;
}

Trong kịch bản trên thread1 sửa đổi biến counter và thread2 đọc biến counter (nhưng không bao giờ sửa đổi nó). Khai báo biến volatile là đủ để đảm bảo khả năng hiển thị cho thread2.
Tuy nhiên, nếu cả thread1 va thread2 đều tăng biến counter, thì việc khai báo biến volatile là không đủ. Ngay cả khi từ khóa volatile đảm bảo rằng tất cả các lần đọc một biến volatile được đọc trực tiếp từ bộ nhớ chính và tất cả ghi vào một biến volatile được ghi trực tiếp vào bộ nhớ chính.

Trong tình huống được giải thích trước đó khi chỉ có thread1 ghi vào biến counter, thì việc khai báo biến counter với volatile là đủ để đảm bảo rằng thread2 luôn nhìn thấy giá trị mới nhất.

Trong thực tế, khi một thread cần đọc giá trị của một biến volatile trước tiên sau đó dựa trên giá trị đó sẽ tạo ra giá trị mới cho biến volatile này, thì biến volatile không còn đủ để đảm bảo khả năng hiển thị chính xác. Khoảng cách thời gian ngắn giữa việc đọc biến volatile và ghi giá trị mới của nó, tạo ra một điều kiện cuộc đua thời gian giữa việc đọc và ghi, chưa kể đến trong đó nhiều luồng có thể đọc cùng một giá trị của một biến volatile trong bộ nhớ chính, sau đó tạo ra một giá trị mới cho biến và khi viết giá trị trở lại bộ nhớ chính - thì khi đó việc ghi đè lên các giá trị của nhau rất có thể sẽ xảy ra.

Tình huống trong đó nhiều luồng đang cùng tăng biến counter chính xác là tình huống như vậy. Các phần sau đây giải thích trường hợp này chi tiết hơn.

Hãy tưởng tượng nếu Thread 1 đọc một biến counter được bộ nhớ chính chia sẻ với giá trị 0 vào bộ đệm CPU của nó (cpu của thread1), tăng nó lên 1 và chưa kịp ghi lại giá trị đã thay đổi vào bộ nhớ chính. Cùng thời điểm đó, luồng 2 có thể đọc cùng một biến counter từ bộ nhớ chính trong đó giá trị của biến vẫn là 0 chia sẻ vào bộ đệm CPU của chính nó (cpu của thread2). Thread2 sau đó cũng có thể tăng biến counter lên 100300. Tình huống này được minh họa trong sơ đồ dưới đây:

Lúc này thread1 và thread 2 không thực sự đồng bộ. Giờ đây ngay cả khi các luồng ghi biến counter từ cpu của chính chúng trở lại bộ nhớ chính thì giá trị này vẫn sai.

Vậy khi nào thì sử dụng biến volatile

Nếu hai hoặc nhiều thread vừa đọc và ghi vào một biến được chia sẻ giữa các thread, thì sử dụng volatile cho điều đó là không đủ. Ta cần sử dụng synchronized trong trường hợp đó để đảm bảo rằng việc đọc và viết biến là nguyên tử.