Thành ngữ của bạn là an toàn nếu và chỉ khi tham chiếu đến HashMap
được công bố an toàn . Thay vì bất cứ điều gì liên quan đến nội bộ của HashMap
chính nó, ấn phẩm an toàn liên quan đến cách chủ đề xây dựng làm cho tham chiếu đến bản đồ hiển thị cho các chủ đề khác.
Về cơ bản, cuộc đua duy nhất có thể ở đây là giữa việc xây dựng HashMap
và bất kỳ chủ đề đọc nào có thể truy cập nó trước khi nó được xây dựng đầy đủ. Hầu hết các cuộc thảo luận là về những gì xảy ra với trạng thái của đối tượng bản đồ, nhưng điều này không liên quan vì bạn không bao giờ sửa đổi nó - vì vậy phần thú vị duy nhất là cách HashMap
tham chiếu được xuất bản.
Ví dụ: hãy tưởng tượng bạn xuất bản bản đồ như thế này:
class SomeClass {
public static HashMap<Object, Object> MAP;
public synchronized static setMap(HashMap<Object, Object> m) {
MAP = m;
}
}
... và tại một số điểm setMap()
được gọi bằng bản đồ và các luồng khác đang sử dụng SomeClass.MAP
để truy cập bản đồ và kiểm tra null như thế này:
HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
.. use the map
} else {
.. some default behavior
}
Điều này không an toàn mặc dù nó có thể xuất hiện như thể nó là. Vấn đề là không có mối quan hệ xảy ra trước khi tập hợp SomeObject.MAP
và lần đọc tiếp theo trên một luồng khác, vì vậy luồng đọc được tự do xem bản đồ được xây dựng một phần. Điều này có thể làm bất cứ điều gì và thậm chí trong thực tế, nó thực hiện những việc như đưa luồng đọc vào một vòng lặp vô hạn .
Để xuất bản bản đồ một cách an toàn, bạn cần thiết lập mối quan hệ xảy ra trước khi viết tham chiếu đến HashMap
(tức là ấn phẩm ) và các độc giả tiếp theo của tài liệu tham khảo đó (nghĩa là mức tiêu thụ). Thuận tiện, chỉ có một vài cách dễ nhớ để thực hiện điều đó [1] :
- Trao đổi tham chiếu thông qua trường được khóa đúng cách ( JLS 17.4.5 )
- Sử dụng trình khởi tạo tĩnh để thực hiện các cửa hàng khởi tạo ( JLS 12.4 )
- Trao đổi tham chiếu qua trường biến động ( JLS 17.4.5 ) hoặc là hệ quả của quy tắc này, thông qua các lớp AtomicX
- Khởi tạo giá trị vào trường cuối cùng ( JLS 17.5 ).
Những cái thú vị nhất cho kịch bản của bạn là (2), (3) và (4). Cụ thể, (3) áp dụng trực tiếp cho mã tôi có ở trên: nếu bạn chuyển đổi khai báo MAP
thành:
public static volatile HashMap<Object, Object> MAP;
sau đó tất cả mọi thứ đều hoàn hảo: độc giả thấy một không null giá trị nhất thiết phải có một xảy ra-trước khi mối quan hệ với các cửa hàng để MAP
và do đó thấy tất cả các cửa hàng liên quan đến việc khởi tạo bản đồ.
Các phương thức khác thay đổi ngữ nghĩa của phương thức của bạn, vì cả (2) (sử dụng bộ khởi động tĩnh) và (4) (sử dụng cuối cùng ) ngụ ý rằng bạn không thể thiết lập MAP
động khi chạy. Nếu bạn không cần phải làm điều đó, thì chỉ cần khai báo MAP
là static final HashMap<>
và bạn được đảm bảo xuất bản an toàn.
Trong thực tế, các quy tắc rất đơn giản để truy cập an toàn vào "các đối tượng không bao giờ sửa đổi":
Nếu bạn đang xuất bản một đối tượng vốn không phải là bất biến (như trong tất cả các trường được khai báo final
) và:
- Bạn đã có thể tạo đối tượng sẽ được chỉ định tại thời điểm khai báo a : chỉ cần sử dụng một
final
trường (bao gồm cả static final
các thành viên tĩnh).
- Bạn muốn gán đối tượng sau, sau khi tham chiếu đã hiển thị: sử dụng trường biến động b .
Đó là nó!
Trong thực tế, nó rất hiệu quả. static final
Ví dụ, việc sử dụng một trường cho phép JVM giả sử giá trị không thay đổi trong vòng đời của chương trình và tối ưu hóa nó rất nhiều. Việc sử dụng final
trường thành viên cho phép hầu hết các kiến trúc đọc trường theo cách tương đương với trường đọc bình thường và không ức chế tối ưu hóa thêm c .
Cuối cùng, việc sử dụng volatile
có một số tác động: không cần rào cản phần cứng đối với nhiều kiến trúc (như x86, cụ thể là không cho phép đọc để vượt qua đọc), nhưng một số tối ưu hóa và sắp xếp lại có thể không xảy ra trong thời gian biên dịch - nhưng điều này hiệu quả nói chung là nhỏ. Đổi lại, bạn thực sự nhận được nhiều hơn những gì bạn yêu cầu - không chỉ bạn có thể xuất bản một cách an toàn HashMap
, bạn có thể lưu trữ nhiều HashMap
s chưa được sửa đổi như bạn muốn cùng một tham chiếu và yên tâm rằng tất cả người đọc sẽ thấy một bản đồ được xuất bản an toàn .
Để biết thêm chi tiết, hãy tham khảo Shipilev hoặc Câu hỏi thường gặp này của Manson và Goetz .
[1] Trích dẫn trực tiếp từ shipilev .
a Nghe có vẻ phức tạp, nhưng ý tôi là bạn có thể gán tham chiếu tại thời điểm xây dựng - tại điểm khai báo hoặc trong hàm tạo (trường thành viên) hoặc bộ khởi tạo tĩnh (trường tĩnh).
b Tùy chọn, bạn có thể sử dụng một synchronized
phương thức để lấy / đặt, hoặc một AtomicReference
cái gì đó, nhưng chúng tôi đang nói về công việc tối thiểu bạn có thể làm.
c Một số kiến trúc với các mô hình bộ nhớ rất yếu (tôi đang nhìn vào bạn , Alpha) có thể yêu cầu một số loại rào cản đọc trước khi final
đọc - nhưng ngày nay chúng rất hiếm.