2012-01-24 17 views
10

Khi tôi đọc bài viết Wikipedia về Double Checked Locking thành ngữ, tôi là nhầm lẫn về nó thực hiện:Tại sao khóa kiểm tra kép này được triển khai với lớp trình bao bọc riêng biệt?

public class FinalWrapper<T> { 
    public final T value; 
    public FinalWrapper(T value) { 
     this.value = value; 
    } 
} 
public class Foo { 
    private FinalWrapper<Helper> helperWrapper = null; 

    public Helper getHelper() { 
     FinalWrapper<Helper> wrapper = helperWrapper; 

    if (wrapper == null) { 
     synchronized(this) { 
      if (helperWrapper == null) { 
       helperWrapper = new FinalWrapper<Helper>(new Helper()); 
      } 
      wrapper = helperWrapper; 
     } 
    } 
    return wrapper.value; 
    } 
} 

Tôi chỉ đơn giản là không hiểu tại sao chúng ta cần tạo wrapper. Không đủ sao?

if (helperWrapper == null) { 
    synchronized(this) { 
     if (helperWrapper == null) { 
      helperWrapper = new FinalWrapper<Helper>(new Helper()); 
     } 
    } 
}  

Có phải vì việc sử dụng trình bao bọc có thể tăng tốc khởi tạo vì trình bao bọc được lưu trữ trên ngăn xếp và helperWrapper được lưu trữ trong heap?

+0

@pst: Tôi nghĩ anh ấy đang hỏi về biến địa phương 'wrapper' bổ sung chứa một bản sao của' helperWrapper'. – casablanca

+0

@casablanca Oh, hum, bỏ lỡ góc đó hoàn toàn .... bây giờ tôi đang bối rối, do phần còn lại của từ ngữ của câu hỏi. "Tôi không hiểu tại sao chúng ta cần phải tạo ra một wrapper." :( –

+0

Câu hỏi hay bằng cách này Chào mừng bạn đến với SO! – Voo

Trả lời

1

Điều này chưa đủ?

if (helperWrapper == null) { 
    synchronized(this) { 
     if (helperWrapper == null) { 
      helperWrapper = new FinalWrapper<Helper>(new Helper()); 
     } 
    } 
} 

Không có điều này là không đủ.

Ở trên, trước tiên hãy kiểm tra helperWrapper == null không phải là chủ đề an toàn. Nó có thể trả về false (nhìn thấy thể hiện không null) cho một số luồng "quá sớm", trỏ tới đối tượng helperWrapper không được xây dựng đầy đủ.

Các rất Wikipedia article bạn tham khảo, giải thích vấn đề này bước-by-step:

Ví dụ, hãy xem xét các chuỗi sự kiện sau:

  1. Chủ đề Một thông báo rằng giá trị không phải là được khởi tạo, vì vậy nó lấy khóa và bắt đầu khởi tạo giá trị.
  2. Do ngữ nghĩa của một số ngôn ngữ lập trình, mã được tạo bởi trình biên dịch được phép cập nhật biến được chia sẻ thành trỏ đến đối tượng được xây dựng một phần trước khi A hoàn thành việc thực hiện khởi tạo .
  3. Chủ đề B thông báo rằng biến chia sẻ đã được khởi tạo (hoặc nó xuất hiện) và trả về giá trị của nó. Vì luồng B tin rằng giá trị đã được khởi tạo nên nó không có khóa. Nếu B sử dụng đối tượng trước khi tất cả quá trình khởi tạo được thực hiện bởi A được xem bởi B (hoặc vì A chưa hoàn tất khởi tạo hoặc vì một số giá trị được khởi tạo trong đối tượng chưa được vi phạm vào bộ nhớ B sử dụng (cache coherence)), chương trình có thể sẽ bị lỗi.

Note ngữ nghĩa của một số ngôn ngữ lập trình đề cập ở trên là chính xác ngữ nghĩa của Java như các phiên bản 1.5 và cao hơn. Mô hình bộ nhớ Java (JSR-133) cho phép hành vi như vậy một cách rõ ràng - tìm kiếm trên web để biết thêm chi tiết về điều đó nếu bạn quan tâm.

Có phải vì việc sử dụng trình bao bọc có thể tăng tốc khởi tạo vì trình bao bọc được lưu trữ trên ngăn xếp và helperWrapper được lưu trữ trong heap?

Không, ở trên không phải là lý do.

Lý do là an toàn luồng. Một lần nữa, ngữ nghĩa của Java 1.5 và cao hơn (như được định nghĩa trong Mô hình bộ nhớ Java) đảm bảo rằng bất kỳ chuỗi nào sẽ chỉ có thể truy cập vào cá thể trình trợ giúp được khởi tạo đúng từ trình bao bọc do thực tế là trường cuối cùng được khởi tạo trong hàm dựng - xem JLS 17.5 Final Field Semantics.

+1

Tôi tin rằng bạn có nghĩa là 'Nó có thể trả về _false_ cho một số chuỗi ...' trong văn bản sau câu 'helperWrapper == null' – Cyberfox

+0

@Cyberfox bạn nói đúng - Tôi đã sửa chữa lỗi đánh máy – gnat

+0

"Nó có thể trả về false (xem trường hợp không null) cho một số chuỗi" quá sớm ", trỏ đến trường hợp không được xây dựng đầy đủ của Trình trợ giúp." <- Làm thế nào có thể "không được xây dựng đầy đủ Ví dụ "bị rò rỉ ở đây? :( –

-1

Hiểu biết của tôi là lý do wrapper được sử dụng là đọc một đối tượng bất biến (tất cả các trường là cuối cùng) là một hoạt động nguyên tử.

Nếu wrapper là không, thì đó chưa phải là đối tượng bất biến và chúng tôi phải rơi vào khối synchronized.

Nếu wrapper không phải là không thì chúng tôi được đảm bảo để có đối tượng value được xây dựng hoàn chỉnh, vì vậy chúng tôi có thể trả lại.

Trong mã của bạn, kiểm tra null có thể được thực hiện mà không thực sự đọc đối tượng được tham chiếu và do đó không kích hoạt tính nguyên tử của thao tác. I E. hoạt động có thể đã khởi tạo helperWrapper để chuẩn bị chuyển kết quả của new Helper() tới hàm tạo, nhưng hàm tạo chưa được gọi.

Bây giờ tôi sẽ đoán rằng mã tiếp theo trong ví dụ của bạn sẽ đọc return helperWrapper.value;nên kích hoạt đọc tham khảo nguyên tử, đảm bảo rằng các nhà xây dựng hoàn tất, nhưng nó hoàn toàn có thể ('ngữ nghĩa của một số ngôn ngữ lập trình') mà trình biên dịch được phép tối ưu hóa để không thực hiện đọc nguyên tử, và do đó nó sẽ trả về một đối tượng value không hoàn toàn được khởi tạo trong các trường hợp chính xác.

Thực hiện rào chắn bằng biến cục bộ và bản sao tham chiếu buộc đọc và ghi là nguyên tử và đảm bảo mã an toàn.

Tôi tin rằng sự hiểu biết quan trọng là tham chiếu bất biến đọc và viết là nguyên tử, do đó gán cho biến khác là nguyên tử, nhưng thử nghiệm null có thể không.

2

Khi tôi hiểu mã, địa phương wrapper thực sự không cần thiết. Lý do của tôi: Lý do thủ thuật Wrapper hoạt động ở vị trí đầu tiên là vì JLS đảm bảo rằng trình biên dịch có thể không xuất bản một tham chiếu tới một đối tượng trước khi tất cả các trường cuối cùng của nó được khởi tạo chính xác. Do đó chúng tôi có bảo đảm rằng helperWrapper/wrapper là không HOẶC helperWrapper/wrapper.value trỏ đến đối tượng được khởi tạo chính xác. Giả sử rằng thức không có bảo lãnh thêm các địa phương sẽ không làm bất cứ điều gì hoặc là: Trình biên dịch sẽ hoàn toàn theo đúng nghĩa của nó để gán biến một phần tạo ra để cả hai wrapperhelperWrapper

Do đó các địa phương nên dư thừa và không ảnh hưởng đến tính chính xác của mã.

+0

trong bài viết Wiki, nó nói "Các wrapper biến địa phương là cần thiết cho tính chính xác." Vì vậy, tôi vẫn nghĩ rằng wrapper là cần thiết. – Audrey

+0

@Audrey Yeah và như chúng ta đều biết wikipedia không bao giờ sai. Cho đến nay không ai có thể nêu ra bất kỳ lý lẽ tốt nào tại sao địa phương lại cần thiết. Timesequence được trích dẫn từ wikipedia trong câu trả lời của gnat ** không thể xảy ra với các biến cuối cùng trong mô hình bộ nhớ Java. – Voo

+0

@Voo Đơn giản chỉ cần sử dụng helperWrapper cho cả hai kiểm tra null và báo cáo trả về có thể thất bại do đọc sắp xếp lại được cho phép theo Mô hình bộ nhớ Java. Tôi đã cập nhật bài viết Wiki bằng một liên kết tham chiếu. –

1

Chỉ cần sử dụng helperWrapper cho cả hai kiểm tra null và câu lệnh trả về có thể không thành công do đọc sắp xếp lại được cho phép trong Mô hình bộ nhớ Java.

Đây là kịch bản ví dụ:

  1. Các helperWrapper == null (là thiếu đứng đắn đã đọc) thử nghiệm đầu tiên để đánh giá sai, ví dụ: helperWrapper không phải là null.
  2. Dòng cuối cùng, return helperWrapper.value (đọc đọc) kết quả trong một NullPointerException, tức làhelperWrapper is null

Điều đó xảy ra như thế nào? Mô hình bộ nhớ Java cho phép hai người đọc đọc được sắp xếp lại, bởi vì không có rào cản nào trước khi đọc, tức là không có mối quan hệ "xảy ra-trước". (Xem String.hashCode example)

Lưu ý rằng trước khi bạn có thể đọc helperWrapper.value, bạn phải đọc chính tài liệu tham khảo helperWrapper. Vì vậy, các đảm bảo được cung cấp bởi final ngữ nghĩa mà helperWrapper được khởi tạo hoàn toàn không áp dụng, vì chúng chỉ áp dụng khi helperWrapper không rỗng.

+0

nó có nghĩa là bộ hành động helpWrapper cho trình bao bọc không cho phép sắp xếp lại lần đọc do ngữ nghĩa cuối cùng? – GhostFlying

+0

@GhostFlying không. các lần đọc của helperWrapper không thể được sắp xếp lại khi chúng ta viết 'return wrapper.value' vì nó không còn đọc helperWrapper nữa. –

Các vấn đề liên quan