Tôi đồng ý với một trong những nhận xét của John: Bạn phải luôn sử dụng một giả khóa cuối cùng trong khi truy cập một biến không phải là cuối cùng để ngăn chặn sự mâu thuẫn trong trường hợp thay đổi tham chiếu của biến. Vì vậy, trong mọi trường hợp và theo nguyên tắc đầu tiên:
Quy tắc # 1: Nếu một trường không phải là trường cuối cùng, hãy luôn sử dụng giả khóa cuối cùng (riêng tư).
Lý do số 1: Bạn giữ khóa và tự mình thay đổi tham chiếu của biến. Một luồng khác đang đợi bên ngoài khóa được đồng bộ hóa sẽ có thể vào khối được bảo vệ.
Lý do # 2: Bạn giữ khóa và một luồng khác thay đổi tham chiếu của biến. Kết quả là như nhau: Một luồng khác có thể vào khối được bảo vệ.
Nhưng khi sử dụng giả khóa cuối cùng, có một vấn đề khác : Bạn có thể nhận được dữ liệu sai, vì đối tượng không phải cuối cùng của bạn sẽ chỉ được đồng bộ hóa với RAM khi gọi đồng bộ hóa (đối tượng). Vì vậy, như một quy tắc ngón tay cái thứ hai:
Quy tắc # 2: Khi khóa một đối tượng không phải là cuối cùng, bạn luôn cần thực hiện cả hai: Sử dụng giả khóa cuối cùng và khóa của đối tượng không phải cuối cùng để đồng bộ hóa RAM. (Cách thay thế duy nhất sẽ là khai báo tất cả các trường của đối tượng là dễ bay hơi!)
Những ổ khóa này còn được gọi là "ổ khóa lồng nhau". Lưu ý rằng bạn phải gọi chúng luôn theo cùng một thứ tự, nếu không bạn sẽ bị khóa chết :
public class X {
private final LOCK;
private Object o;
public void setO(Object o){
this.o = o;
}
public void x() {
synchronized (LOCK) {
synchronized(o){
}
}
}
}
Như bạn có thể thấy, tôi viết hai ổ khóa trực tiếp trên cùng một dòng, bởi vì chúng luôn thuộc về nhau. Như thế này, bạn thậm chí có thể làm 10 khóa lồng nhau:
synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
}
}
}
}
Lưu ý rằng mã này sẽ không bị phá vỡ nếu bạn chỉ có một khóa bên trong giống như synchronized (LOCK3)
một chuỗi khác. Nhưng nó sẽ bị hỏng nếu bạn gọi trong một chuỗi khác như thế này:
synchronized (LOCK4) {
synchronized (LOCK1) {
synchronized (LOCK3) {
synchronized (LOCK2) {
}
}
}
}
Chỉ có một cách giải quyết xung quanh các khóa lồng nhau như vậy trong khi xử lý các trường không phải cuối cùng:
Quy tắc số 2 - Thay thế: Khai báo tất cả các trường của đối tượng là biến động. (Tôi sẽ không nói ở đây về những bất lợi của việc này, ví dụ như ngăn chặn bất kỳ bộ nhớ nào trong bộ nhớ đệm cấp x ngay cả đối với các lần đọc, aso.)
Vì vậy, do đó aioobe khá đúng: Chỉ cần sử dụng java.util.concurrent. Hoặc bắt đầu hiểu mọi thứ về đồng bộ hóa và tự mình làm điều đó với các ổ khóa lồng nhau. ;)
Để biết thêm chi tiết tại sao đồng bộ hóa trên các trường không phải cuối cùng bị ngắt, hãy xem trường hợp thử nghiệm của tôi: https://stackoverflow.com/a/21460055/2012947
Và để biết thêm chi tiết tại sao bạn cần đồng bộ hóa do RAM và bộ nhớ đệm, hãy xem tại đây: https://stackoverflow.com/a/21409975/2012947
o
được đề cập tại thời điểm đạt đến khối được đồng bộ hóa. Nếu đối tượngo
đề cập đến các thay đổi, một luồng khác có thể đến và thực thi khối mã được đồng bộ hóa.