Điều quan trọng là phải hiểu rằng có hai khía cạnh về an toàn luồng.
- kiểm soát thực thi, và
- khả năng hiển thị bộ nhớ
Việc đầu tiên phải thực hiện với việc kiểm soát khi mã thực thi (bao gồm cả thứ tự thực hiện các lệnh) và liệu nó có thể thực thi đồng thời hay không, và thứ hai phải làm khi các hiệu ứng trong bộ nhớ của những gì đã được thực hiện được hiển thị cho các luồng khác. Vì mỗi CPU có một số mức bộ đệm giữa nó và bộ nhớ chính, nên các luồng chạy trên các CPU hoặc lõi khác nhau có thể thấy "bộ nhớ" khác nhau tại bất kỳ thời điểm nào do các luồng được phép lấy và hoạt động trên các bản sao riêng của bộ nhớ chính.
Việc sử dụng synchronized
ngăn không cho bất kỳ luồng nào khác có được màn hình (hoặc khóa) cho cùng một đối tượng , do đó ngăn tất cả các khối mã được bảo vệ bằng cách đồng bộ hóa trên cùng một đối tượng thực hiện đồng thời. Đồng bộ hóa cũng tạo ra một rào cản bộ nhớ "xảy ra trước", gây ra hạn chế về khả năng hiển thị bộ nhớ sao cho mọi thứ được thực hiện đến thời điểm một số luồng phát hành khóa xuất hiện cho một luồng khác sau đó có được khóa tương tự đã xảy ra trước khi khóa. Về mặt thực tế, trên phần cứng hiện tại, điều này thường gây ra lỗi bộ đệm CPU khi có màn hình và ghi vào bộ nhớ chính khi nó được phát hành, cả hai đều đắt (tương đối).
volatile
Mặt khác, sử dụng , buộc tất cả các truy cập (đọc hoặc ghi) vào biến dễ bay hơi xảy ra với bộ nhớ chính, giữ cho biến dễ bay ra khỏi bộ đệm CPU. Điều này có thể hữu ích cho một số hành động trong đó đơn giản là yêu cầu rằng khả năng hiển thị của biến là chính xác và thứ tự truy cập không quan trọng. Sử dụng volatile
cũng thay đổi điều trị long
và double
yêu cầu truy cập vào chúng là nguyên tử; trên một số phần cứng (cũ hơn), điều này có thể yêu cầu khóa, mặc dù không phải trên phần cứng 64 bit hiện đại. Trong mô hình bộ nhớ mới (JSR-133) cho Java 5+, ngữ nghĩa của tính không ổn định đã được tăng cường mạnh mẽ gần như được đồng bộ hóa đối với khả năng hiển thị bộ nhớ và sắp xếp lệnh (xem http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#voliverse). Đối với mục đích hiển thị, mỗi quyền truy cập vào trường biến động hoạt động giống như một nửa đồng bộ hóa.
Trong mô hình bộ nhớ mới, vẫn đúng là các biến dễ bay hơi không thể được sắp xếp lại với nhau. Sự khác biệt là bây giờ không còn dễ dàng để sắp xếp lại các truy cập trường bình thường xung quanh chúng. Viết vào trường dễ bay hơi có hiệu ứng bộ nhớ tương tự như bản phát hành màn hình và đọc từ trường dễ bay hơi có hiệu ứng bộ nhớ tương tự như màn hình thu được. Trong thực tế, bởi vì mô hình bộ nhớ mới đặt các ràng buộc chặt chẽ hơn trong việc sắp xếp lại các truy cập trường dễ bay hơi với các truy cập trường khác, dễ bay hơi hoặc không, bất cứ điều gì có thể nhìn thấy đối với luồng A
khi nó ghi vào trường dễ bay hơi f
sẽ hiển thị cho luồng B
khi đọc f
.
- Câu hỏi thường gặp về JSR 133 (Mô hình bộ nhớ Java)
Vì vậy, bây giờ cả hai dạng rào cản bộ nhớ (theo JMM hiện tại) đều tạo ra một rào cản sắp xếp lại lệnh để ngăn trình biên dịch hoặc thời gian chạy sắp xếp lại các lệnh theo hàng rào. Trong JMM cũ, không ổn định không ngăn cản việc đặt hàng lại. Điều này có thể quan trọng, bởi vì ngoài các rào cản bộ nhớ, giới hạn duy nhất được đặt ra là, đối với bất kỳ luồng cụ thể nào , hiệu ứng ròng của mã cũng giống như nếu các lệnh được thực thi theo thứ tự chính xác mà chúng xuất hiện trong nguồn.
Một cách sử dụng dễ bay hơi là cho một đối tượng được chia sẻ nhưng không thay đổi được tạo lại một cách nhanh chóng, với nhiều luồng khác tham chiếu đến đối tượng tại một điểm cụ thể trong chu kỳ thực hiện của chúng. Người ta cần các luồng khác để bắt đầu sử dụng đối tượng được tạo lại sau khi nó được xuất bản, nhưng không cần thêm chi phí đồng bộ hóa đầy đủ và đó là sự tranh chấp tùy ý và xóa bộ đệm.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Nói với câu hỏi đọc-cập nhật-viết của bạn, cụ thể. Hãy xem xét các mã không an toàn sau:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Bây giờ, với phương thức updateCorer () không được đồng bộ hóa, hai luồng có thể nhập cùng một lúc. Trong số nhiều hoán vị của những gì có thể xảy ra, một là luồng-1 thực hiện kiểm tra bộ đếm == 1000 và thấy nó đúng và sau đó bị treo. Sau đó, luồng-2 thực hiện cùng một bài kiểm tra và cũng thấy nó đúng và bị treo. Sau đó, thread-1 tiếp tục và đặt bộ đếm thành 0. Sau đó, thread-2 lại tiếp tục đặt bộ đếm thành 0 vì nó đã bỏ lỡ bản cập nhật từ thread-1. Điều này cũng có thể xảy ra ngay cả khi chuyển đổi luồng không xảy ra như tôi đã mô tả, nhưng đơn giản là vì hai bản sao bộ đếm được lưu trong bộ nhớ cache khác nhau có mặt trong hai lõi CPU khác nhau và mỗi luồng chạy trên một lõi riêng biệt. Đối với vấn đề đó, một luồng có thể có bộ đếm ở một giá trị và luồng kia có thể có bộ đếm ở một giá trị hoàn toàn khác chỉ vì bộ nhớ đệm.
Điều quan trọng trong ví dụ này là bộ đếm biến được đọc từ bộ nhớ chính vào bộ đệm, được cập nhật trong bộ đệm và chỉ được ghi lại vào bộ nhớ chính tại một số điểm không xác định sau đó khi xảy ra rào cản bộ nhớ hoặc khi cần bộ nhớ đệm cho thứ khác. Việc tạo bộ đếm volatile
là không đủ cho tính an toàn của luồng của mã này, bởi vì kiểm tra mức tối đa và các bài tập là các hoạt động riêng biệt, bao gồm cả phần tăng là một tập hợp các read+increment+write
hướng dẫn máy không nguyên tử , đại loại như:
MOV EAX,counter
INC EAX
MOV counter,EAX
Các biến dễ bay hơi chỉ hữu ích khi tất cả các hoạt động được thực hiện trên chúng là "nguyên tử", chẳng hạn như ví dụ của tôi trong đó tham chiếu đến một đối tượng được hình thành đầy đủ chỉ được đọc hoặc viết (và thực tế, thông thường, nó chỉ được viết từ một điểm duy nhất). Một ví dụ khác sẽ là một tham chiếu mảng dễ bay hơi sao lưu danh sách sao chép trên ghi, với điều kiện mảng chỉ được đọc bằng cách lấy một bản sao cục bộ của tham chiếu đến nó.