volatile-java

Từ khóa Volatile trong Java là gì?

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.

Tổng quan

Trong trường hợp không có đồng bộ hóa cần thiết, trình biên dịch, thời gian chạy hoặc bộ xử lý có thể áp dụng tất cả các loại tối ưu hóa. Mặc dù những tối ưu hóa này hầu hết đều có lợi, nhưng đôi khi chúng có thể gây ra các vấn đề tế nhị.

Lưu vào bộ nhớ đệm và sắp xếp lại là một trong những cách tối ưu hóa có thể khiến chúng ta ngạc nhiên trong các bối cảnh đồng thời. Java và JVM cung cấp nhiều cách để kiểm soát thứ tự bộ nhớ, và từ khóa biến đổi là một trong số đó.

Trong hướng dẫn này, tôi sẽ tập trung vào khái niệm cơ bản, nhưng thường bị hiểu lầm, trong ngôn ngữ Java, từ khóa biến động. Đầu tiên, chúng ta sẽ bắt đầu với một chút thông tin cơ bản về cách thức hoạt động của kiến ​​trúc máy tính bên dưới, và sau đó chúng ta sẽ làm quen với thứ tự bộ nhớ trong Java.

Kiến trúc đa xử lý dùng chung

Bộ xử lý có trách nhiệm thực hiện các lệnh của chương trình. Do đó, họ cần lấy cả hướng dẫn chương trình và dữ liệu cần thiết từ RAM.

Vì các CPU có khả năng thực hiện một số lượng lớn các lệnh mỗi giây, nên việc tìm nạp từ RAM không phải là điều lý tưởng cho chúng. Để cải thiện tình trạng này, các bộ xử lý đang sử dụng các thủ thuật như Thực hiện ngoài lệnh, Dự đoán nhánh, Thực hiện đầu cơ và tất nhiên, lưu vào bộ nhớ đệm.

Đây là lúc hệ thống phân cấp bộ nhớ sau hoạt động:

Khi các lõi khác nhau thực thi nhiều lệnh hơn và thao tác nhiều dữ liệu hơn, chúng sẽ lấp đầy bộ nhớ đệm của mình bằng các dữ liệu và hướng dẫn có liên quan hơn. Điều này sẽ cải thiện hiệu suất tổng thể với chi phí đưa ra các thách thức về tính liên kết của bộ nhớ cache.

Nói một cách đơn giản, chúng ta nên suy nghĩ kỹ về điều gì sẽ xảy ra khi một luồng cập nhật một giá trị được lưu trong bộ nhớ cache.

Khi nào sử dụng từ khóa volatile?

Để mở rộng thêm về tính thống nhất của bộ nhớ cache, tôi sẽ mượn một ví dụ từ cuốn sách Java Concurrency in Practice:

public class TaskRunner { private static int number; private static boolean ready; private static class Reader extends Thread { @Override public void run() { while (!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new Reader().start(); number = 42; ready = true; } }
Code language: PHP (php)

Lớp TaskRunner duy trì hai biến đơn giản. Trong phương thức chính của nó, nó tạo ra một luồng khác quay trên biến sẵn sàng miễn là nó sai. Khi biến trở thành true, luồng sẽ chỉ cần in biến số.

Nhiều người có thể mong đợi chương trình này chỉ in 42 sau một thời gian ngắn; tuy nhiên, trên thực tế, thời gian trì hoãn có thể lâu hơn nhiều. Nó thậm chí có thể bị treo vĩnh viễn hoặc in số không.

Khả năng hiển thị bộ nhớ

Trong ví dụ đơn giản này, chúng ta có hai luồng ứng dụng: luồng chính và luồng người đọc. Hãy tưởng tượng một kịch bản trong đó HĐH lập lịch các luồng đó trên hai lõi CPU khác nhau, trong đó:

  • Luồng chính có bản sao của các biến sẵn sàng và số trong bộ nhớ cache cốt lõi của nó.
  • Chuỗi người đọc cũng kết thúc với các bản sao của nó.
  • Chuỗi chính cập nhật các giá trị được lưu trong bộ nhớ cache.

Trên hầu hết các bộ xử lý hiện đại, yêu cầu ghi sẽ không được áp dụng ngay sau khi chúng được phát hành. Trên thực tế, các bộ xử lý có xu hướng xếp hàng những lần ghi trong một bộ đệm ghi đặc biệt. Sau một thời gian, chúng sẽ áp dụng tất cả các ghi đó vào bộ nhớ chính cùng một lúc.

Với tất cả những gì đã nói, khi luồng chính cập nhật số lượng và các biến sẵn sàng, không có gì đảm bảo về những gì mà luồng người đọc có thể thấy. Nói cách khác, luồng người đọc có thể thấy giá trị được cập nhật ngay lập tức, hoặc với một số độ trễ, hoặc không bao giờ.

Khả năng hiển thị bộ nhớ này có thể gây ra các vấn đề về độ sống trong các chương trình dựa vào khả năng hiển thị.

Reordering

Để làm cho vấn đề thậm chí còn tồi tệ hơn, luồng người đọc có thể thấy những thứ đó được ghi theo thứ tự khác với thứ tự chương trình thực tế. Ví dụ: vì lần đầu tiên chúng tôi cập nhật biến số:

public static void main(String[] args) { new Reader().start(); number = 42; ready = true; }
Code language: JavaScript (javascript)

Chúng tôi có thể mong đợi luồng đầu đọc in 42. Nhưng, thực tế có thể xem số 0 là giá trị được in.

Sắp xếp lại thứ tự là một kỹ thuật tối ưu hóa để cải thiện hiệu suất. Thật thú vị, các thành phần khác nhau có thể áp dụng tối ưu hóa này:

  • Bộ xử lý có thể xóa bộ đệm ghi của nó theo thứ tự khác với thứ tự chương trình.
  • Bộ xử lý có thể áp dụng kỹ thuật thực thi không theo thứ tự.
  • Trình biên dịch JIT có thể tối ưu hóa thông qua sắp xếp lại.

volatile Memory Order

Để đảm bảo rằng các bản cập nhật cho các biến truyền đi một cách có thể dự đoán được đến các luồng khác, chúng ta nên áp dụng công cụ sửa đổi biến động cho các biến đó:

public class TaskRunner { private volatile static int number; private volatile static boolean ready; // same as before }
Code language: PHP (php)

Bằng cách này, tôi giao tiếp với thời gian chạy và bộ xử lý để không sắp xếp lại bất kỳ lệnh nào liên quan đến biến volatile. Ngoài ra, bộ xử lý hiểu rằng họ nên cập nhật bất kỳ bản cập nhật nào cho các biến này ngay lập tức.

Đối với các ứng dụng đa luồng, chúng ta cần đảm bảo một số quy tắc để có hành vi nhất quán:

Volatile và đồng bộ hóa luồng

  • Loại trừ lẫn nhau – chỉ một chuỗi thực thi một phần quan trọng tại một thời điểm.
  • Mức độ hiển thị – các thay đổi được thực hiện bởi một chuỗi đối với dữ liệu được chia sẻ sẽ hiển thị với các chuỗi khác để duy trì tính nhất quán của dữ liệu.

các phương thức và khối được đồng bộ hóa cung cấp cả hai thuộc tính trên với chi phí là hiệu suất của ứng dụng.

Volatile là một từ khóa khá hữu ích vì nó có thể giúp đảm bảo khía cạnh hiển thị của dữ liệu thay đổi mà không cung cấp loại trừ lẫn nhau. Do đó, nó hữu ích ở những nơi mà chúng ta ổn với nhiều luồng thực thi song song một khối mã, nhưng chúng ta cần đảm bảo thuộc tính khả năng hiển thị.

Happens-Before Ordering

Các hiệu ứng khả năng hiển thị bộ nhớ của các biến volatile mở rộng ra ngoài bản thân các biến số volatile.

Để làm cho vấn đề cụ thể hơn, hãy giả sử luồng A ghi vào một biến dễ thay đổi, và sau đó luồng B đọc cùng một biến biến động. Trong những trường hợp như vậy, các giá trị hiển thị cho A trước khi viết biến biến động sẽ hiển thị cho B sau khi đọc biến biến động:

Nói về mặt kỹ thuật, bất kỳ lần ghi nào vào một trường biến động đều xảy ra trước mỗi lần đọc tiếp theo của cùng một trường. Đây là quy tắc biến dễ thay đổi của Mô hình bộ nhớ Java (JMM).

Piggybacking

Do sức mạnh của thứ tự bộ nhớ xảy ra trước khi xảy ra, đôi khi chúng ta có thể dựa vào các thuộc tính khả năng hiển thị của một biến biến động khác. Ví dụ, trong ví dụ cụ thể của chúng tôi, tôi chỉ cần đánh dấu biến sẵn sàng là biến động:

public class TaskRunner { private static int number; // not volatile private volatile static boolean ready; // same as before }
Code language: PHP (php)

Bất kỳ thứ gì trước khi ghi true vào biến sẵn sàng sẽ hiển thị cho bất kỳ thứ gì sau khi đọc biến sẵn sàng. Do đó, biến số sẽ dựa trên khả năng hiển thị của bộ nhớ được thực thi bởi biến sẵn sàng. Nói một cách đơn giản, mặc dù nó không phải là một biến số volatile, nhưng nó thể hiện một hành vi volatile.

Bằng cách sử dụng những ngữ nghĩa này, chúng ta chỉ có thể xác định một số biến trong lớp của chúng ta là biến động và tối ưu hóa việc đảm bảo khả năng hiển thị.

Tổng kết

Trong bài viết này, tôi đã giải thích từ khóa volatile và các khả năng của nó, cũng như những cải tiến được thực hiện cho nó bắt đầu với Java 5. Chúc bạn học tốt.

Bài viết liên quan

Leave a Reply

Your email address will not be published.