Mục lục
Từ khóa volatile trong java được sử dụng để đánh dấu một biến sẽ được lưu trữ trong main memory. Nghĩa là mọi lệnh đọc giá trị các biến volatile đều đọc từ main menory chứ không phải đọc từ CPU cache, việc ghi giá trị cho các biến volatile cũng tương tự sẽ ghi xuống main memory.
Tại sao cần sử dụng Volatile
Trong ứng dụng đa luồng(multithread) khi mỗi luồng(thread) làm việc với các biến non-volatile(không có chứa từ khóa volatile) mỗi thread có thể sao chép chúng từ main memory vào CPU cache để thao tác giúp tăng performance. Nếu máy tính của bạn có nhiều CPU thì có thể mỗi thread sẽ chạy trên một CPU khác nhau đồng nghĩa với việc mỗi thread sẽ sao chép các biến vào CPU cache riêng của chúng.
Với các biến non-volatile thì chúng ta sẽ không đảm bảo rằng khi bào JVM đọc dữ liệu từ main memory vào cpu cache và ghi dữ liệu từ CPU cache vào main memory. Chính vì thế giá trị của biến non-volatile có thể khác nhau ở từng thread và giá trị của chúng ở main memory cũng ko đảm bảo tính đồng nhất.
Ví dụ chúng ta có 2 thread đều dùng object SharedObject như sau
public class SharedObject { public int counter = 0; }
Giả sử thread 1 sẽ là thread tăng biến counter lên 1 và thread 2 đọc và xử lý. Nếu counter không phải là một volatile biến thì giá trị counter ở thread 1, main memory và thread 2 có khả năng sẽ khác nhau hoàn toàn
Chúng ta thấy thread 1 đã update Counter lên 7 mà thread 2 vẫn đang giữ giá trị 0. Vì Thread 1 update Counter lên CPU cache mà không ghi trực tiếp lên main memory.
Cách sử dụng Volatile
Ở trường hợp trên chúng ta cần Thread 2 luôn đọc được giá trị cuối cùng được Thread 1 update khi đó phải dùng đến volatile. Lưu ý chỉ đối với trường hợp chỉ có thread 1 update giá trị của Counter và thread 2 lấy và sử dụng.
public class SharedObject { public volatile int counter = 0; }
Flow hoạt động của volatile
Nếu Thread A và Thread B thao tác trên cùng một biến volatile, trong đó chỉ có Thread A ghi giá trị vào biến volatile.
1, Nếu Thread A ghi giá trị trên biến volatile thì thread B sẽ đọc lại giá trị mới vừa được ghi xuống bởi thread A.
Ví dụ
class VolatileTest { private static volatile int MY_INT = 0; public static void main(String[] args) { new Thread(() -> { int local_value = MY_INT; while (local_value < 5) { if (local_value != MY_INT) { System.out.println("Thread B, Incrementing MY_INT to:" + MY_INT); local_value = MY_INT; } } }).start(); new Thread(() -> { int local_value = MY_INT; while (MY_INT < 5) { System.out.println("Thread A, Incrementing MY_INT to:" + (local_value + 1)); MY_INT = ++local_value; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
Output:
Thread A, Incrementing MY_INT to:1
Thread B, Incrementing MY_INT to:1
Thread A, Incrementing MY_INT to:2
Thread B, Incrementing MY_INT to:2
Thread A, Incrementing MY_INT to:3
Thread B, Incrementing MY_INT to:3
Thread A, Incrementing MY_INT to:4
Thread B, Incrementing MY_INT to:4
Thread A, Incrementing MY_INT to:5
Thread B, Incrementing MY_INT to:5
Nếu MY_INT không phải là biến volatile thì kết quả là
Output
Thread A, Incrementing MY_INT to:1
Thread B, Incrementing MY_INT to:1
Thread A, Incrementing MY_INT to:2
Thread A, Incrementing MY_INT to:3
Thread A, Incrementing MY_INT to:4
Thread A, Incrementing MY_INT to:5
2, Nếu Thead A đọc giá trị biến volatile từ main memory thì các biến non-volatile cũng đọc lại từ main memory
Ví dụ
public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } }
Method totalDays() đọc giá trị days từ main memory thì giá trị của months và years cũng sẽ được đọc lại từ main memory. Do đó bạn phải đảm bảo rằng giá trị của days phải được đọc đầu tiên để có giá trị cuối cùng từ main memory.
Hạn chế của volatile
Trong thực tế thì có thể xảy ra việc nhiều thread ghi vào biến volatile và sẽ dẫn đến đến giá trị sai bởi vì chuyện đọc ghi đồng thời giữa các thread.
Ví dụ biến counter được khởi tạo tại main memory là 0. Sau đó thread 1 lấy và tăng nó lên 1 cùng lúc đó thread 2 cũng lấy và được giá trị 0 và nó cũng tăng lên 1. Trong khi ở thread 2 giá trị của counter phải là 2.
Chưa kể đến các nếu có nhiều hơn 2 thread và chúng cứ đọc và ghi lại biến counter thì giá trị của nó sẽ không thể nào kiểm soát được.
Khi nào sử dụng volatile
Như đã đề cập ở trên thì chúng ta chỉ sử dụng volatile khi có hai thread trong đó một thread đọc và một thread ghi vào biến volatile còn các trường hợp khác cần sử dụng đến synchronized.
Lưu ý là khi dùng volatile thì performance của bạn sẽ bị giảm đáng kể vì việc thay vì chúng đọc ghi từ cache thì bây giờ chúng phải làm việc với main memory. Hơn nữa chúng còn làm thay đổi các cơ chế tăng cường performance để đáp ứng việc đọc ghi với các biến volatile như mình đã nêu ở trên như là sau khi đọc biến volatile thì các biến non-volatile cũng được đọc lại etc.