34

Tôi đã xảy ra khi một bài báo gần đây thảo luận về mô hình khóa đã kiểm tra đôi trong Java và những cạm bẫy của nó và bây giờ tôi tự hỏi nếu một biến thể của mẫu đó tôi đã sử dụng trong nhiều năm nay .Java đôi kiểm tra khóa

Tôi đã xem nhiều bài viết và bài viết về chủ đề và hiểu các vấn đề tiềm năng khi tham chiếu đến một đối tượng được xây dựng một phần và theo như tôi có thể nói, tôi không nghĩ triển khai của tôi là chủ đề cho những vấn đề này. Có bất kỳ vấn đề nào với mẫu sau không?

Và nếu không, tại sao mọi người không sử dụng? Tôi chưa bao giờ thấy nó được đề nghị trong bất kỳ cuộc thảo luận nào tôi đã thấy xung quanh vấn đề này.

public class Test { 
    private static Test instance; 
    private static boolean initialized = false; 

    public static Test getInstance() { 
     if (!initialized) { 
      synchronized (Test.class) { 
       if (!initialized) { 
        instance = new Test(); 
        initialized = true; 
       } 
      } 
     } 
     return instance; 
    } 
} 
+0

Tôi sử dụng nó. Lý do nó hoạt động là vì cá thể sẽ được khởi tạo đầy đủ trước khi dòng khởi tạo = true được nhấn và gán giá trị boolean trong Java là một hoạt động nguyên tử (trên mọi chipset tôi từng sử dụng). – Jono

+2

@Jono Nó phụ thuộc vào trình biên dịch và máy ảo, nếu nó hoạt động hay không. Trình biên dịch có thể tạo ra một bytecode là _caching_ giá trị của '! Initialized' expression, và do đó thiếu nếu một luồng khác thay đổi giá trị của thuộc tính' initialized'. Nếu cùng một mã được chạy song song, khởi tạo thể hiện có thể được thực hiện hai lần. Ngoài ra, trình biên dịch JIT có thể tối ưu hóa biểu thức này từ một bytecode không được tối ưu hóa. Chỉ cần nghĩ rằng: VM phải đánh giá cùng một biểu thức, ngay sau biểu thức kia. Một hoạt động 'dup' ngăn cản việc đánh giá lại, và là một sự chuyển đổi mã hợp lệ theo JLS. – gaborsch

Trả lời

16

Điều đó sẽ hiệu quả nếu initializedvolatile. Cũng giống như với synchronized các hiệu ứng thú vị của volatile không thực sự quan trọng đối với tham chiếu như những gì chúng ta có thể nói về dữ liệu khác. Thiết lập trường instance và đối tượng Test bị buộc phải xảy ra trước ghi thành initialized. Khi sử dụng giá trị được lưu trong bộ nhớ cache thông qua mạch ngắn, hãy đọc xảy ra trước khi đọc instance và các đối tượng được tiếp cận thông qua tham chiếu. Không có sự khác biệt đáng kể trong việc có một cờ riêng biệt initialized (ngoài việc nó còn gây ra sự phức tạp hơn trong mã).

(Các quy tắc cho final trường trong nhà xây dựng để công bố không an toàn là một chút khác nhau.)

Tuy nhiên, bạn hiếm khi sẽ thấy lỗi trong trường hợp này. Cơ hội gặp rắc rối khi sử dụng lần đầu tiên là tối thiểu, và nó là một cuộc đua không lặp lại.

Mã quá phức tạp. Bạn chỉ có thể viết như sau:

private static final Test instance = new Test(); 

public static Test getInstance() { 
    return instance; 
} 
+3

Nhưng hãy cẩn thận khi tạo ra thế giới khi khởi động ứng dụng. Lazy initialisation có sử dụng của nó. – rsp

+6

@rsp Điều này vẫn còn lười biếng: trong khi bạn không truy cập vào lớp, nó sẽ không khởi tạo. Nhưng đúng là, một số trường hợp, chúng tôi muốn sự lười biếng thực sự ở thời điểm cuối cùng ... – KLE

+0

@Tom - có lỗi đánh máy trên giá trị trả lại của bạn – McDowell

23

Double check locking is broken. Kể từ khi khởi tạo là một nguyên thủy, nó có thể không yêu cầu nó được dễ bay hơi để làm việc, tuy nhiên không có gì ngăn chặn khởi tạo được xem là đúng với mã không được đồng bộ hóa trước khi cá thể được khởi tạo.

EDIT: Để làm rõ câu trả lời ở trên, câu hỏi ban đầu được hỏi về việc sử dụng boolean để kiểm soát khóa kiểm tra kép. Nếu không có các giải pháp trong liên kết ở trên, nó sẽ không hoạt động. Bạn có thể kiểm tra lại khóa thực sự thiết lập một boolean, nhưng bạn vẫn còn có vấn đề về hướng dẫn sắp xếp lại khi nói đến việc tạo ra cá thể lớp. Giải pháp được đề xuất không hoạt động vì trường hợp có thể không được khởi tạo sau khi bạn thấy boolean được khởi tạo là đúng trong khối không được đồng bộ hóa. Các giải pháp thích hợp để kiểm tra lại khóa là sử dụng dễ bay hơi (trên trường thể hiện) và quên về boolean được khởi tạo, và chắc chắn sử dụng JDK 1.5 hoặc cao hơn hoặc khởi tạo nó trong trường cuối cùng, như được xây dựng trong bài viết liên kết và câu trả lời của Tom, hoặc không sử dụng nó. Chắc chắn toàn bộ khái niệm có vẻ như một tối ưu hóa rất lớn trừ khi bạn biết bạn sẽ nhận được một tấn tranh luận về việc này Singleton, hoặc bạn đã lược tả các ứng dụng và đã thấy đây là một điểm nóng.

+35

Khóa đã kiểm tra * đã bị hỏng. Đừng quên đọc phần có tiêu đề "Dưới mô hình bộ nhớ Java mới" ở cuối tài liệu được liên kết của bạn, cho thấy cách thức và lý do nó hoạt động trong JDKs 1.5 trở lên (tức là hơn năm năm nay). –

+1

@dtsazza, có nó có thể được cố định với dễ bay hơi bây giờ, nhưng OP không có điều đó, nó đã cố gắng để giải quyết nó với một boolean. – Yishai

11

Khóa kiểm tra đôi thực sự bị hỏng và giải pháp cho vấn đề thực sự đơn giản hơn để triển khai mã khôn ngoan hơn thành ngữ này - chỉ cần sử dụng trình khởi tạo tĩnh.

public class Test { 
    private static final Test instance = createInstance(); 

    private static Test createInstance() { 
     // construction logic goes here... 
     return new Test(); 
    } 

    public static Test getInstance() { 
     return instance; 
    } 
} 

Trình khởi chạy tĩnh được đảm bảo được thực hiện lần đầu tiên JVM nạp lớp và trước khi tham chiếu lớp có thể được trả về bất kỳ chuỗi nào - làm cho nó trở thành luồng an toàn.

+0

Ví dụ của bạn không chính xác vì trường * 'instance' * không được đánh dấu là * 'final' *, vì vậy, không đảm bảo rằng không có chuỗi nào có thể thấy giá trị * 'null' * ở đó. –

+2

Tôi không nghĩ điều đó là đúng dựa trên http://java.sun.com/docs/books/jls/second_edition/html/execution.doc.html#44557 và http://java.sun.com/docs /books/jls/second_edition/html/execution.doc.html#44630 - công cụ sửa đổi cuối cùng dường như chỉ ảnh hưởng đến thứ tự của các trường được khởi tạo. Tuy nhiên lĩnh vực này nên được cuối cùng anyway trong việc sử dụng của mình, vì vậy tôi đã cập nhật mã bất kể. –

5

Đây là lý do tại sao khóa kiểm tra kép bị hỏng.

Đảm bảo đồng bộ hóa, chỉ một chuỗi có thể nhập một khối mã. Nhưng nó không đảm bảo, các biến sửa đổi được thực hiện trong phần được đồng bộ hóa sẽ hiển thị với các luồng khác. Chỉ các luồng vào khối được đồng bộ mới được đảm bảo để xem các thay đổi. Đây là lý do tại sao khóa kiểm tra kép bị hỏng - nó không được đồng bộ hóa ở phía người đọc. Các chủ đề đọc có thể thấy, rằng singleton không phải là null, nhưng dữ liệu singleton có thể không được khởi tạo đầy đủ (có thể nhìn thấy).

Đặt hàng được cung cấp bởi volatile. volatile đảm bảo đặt hàng, ví dụ ghi vào trường tĩnh đơn biến động đảm bảo rằng ghi vào đối tượng singleton sẽ được hoàn thành trước khi ghi vào trường tĩnh biến động. Nó không ngăn cản việc tạo ra đơn lẻ của hai đối tượng, điều này được cung cấp bởi đồng bộ hóa.

Các trường tĩnh cuối cùng của lớp không cần phải dễ bay hơi. Trong Java, JVM sẽ giải quyết vấn đề này.

Xem bài đăng của tôi, an answer to Singleton pattern and broken double checked locking in a real-world Java application, minh họa ví dụ về một singleton đối với khóa được kiểm tra kép trông thông minh nhưng bị hỏng.

+0

Nếu thay đổi không được đảm bảo để được nhìn thấy trong các chủ đề khác, điều đó có nghĩa là việc đồng bộ hóa chính phương thức đó cũng sẽ bị hỏng không? Tôi nghĩ điều đó được cho là an toàn. –

+0

@BillK Có một chuỗi xảy ra trước khi ghi bất kỳ bên trong khối được đồng bộ hóa vào bất kỳ khối đọc nào được đồng bộ hóa trên cùng một đối tượng. –

0

Nếu "khởi tạo" là đúng, thì "cá thể" PHẢI được khởi tạo hoàn toàn, giống như 1 cộng 1 bằng 2 :). Vì vậy, mã là chính xác. Ví dụ này chỉ được khởi tạo một lần nhưng chức năng có thể được gọi là một triệu lần để nó cải thiện hiệu suất mà không kiểm tra đồng bộ hóa cho một triệu trừ một lần.

0

Vẫn còn một số trường hợp khi kiểm tra kép có thể được sử dụng.

  1. Trước tiên, nếu bạn thực sự không cần một singleton và kiểm tra kép được sử dụng chỉ để KHÔNG tạo và khởi tạo cho nhiều đối tượng.
  2. Có trường final được đặt ở cuối của hàm tạo/khối được khởi tạo (khiến tất cả các trường được khởi tạo trước đó được các chuỗi khác xem trước).
0

Tôi đã điều tra về hai lần kiểm tra khóa thành ngữ và từ những gì tôi hiểu, mã của bạn có thể dẫn đến các vấn đề về đọc một trường hợp xây dựng một phần TRỪ class Test của bạn là bất di bất dịch:

Các Mô hình bộ nhớ Java cung cấp một sự bảo đảm đặc biệt về sự an toàn khởi tạo để chia sẻ các đối tượng bất biến.

Chúng có thể được truy cập an toàn ngay cả khi đồng bộ hóa không được sử dụng để xuất bản tham chiếu đối tượng.

(Báo giá từ cuốn sách rất nên Java Concurrency in Practice)

Vì vậy, trong trường hợp đó, hai lần kiểm tra khóa thành ngữ sẽ làm việc.Tuy nhiên, nếu trường hợp đó không xảy ra, hãy quan sát rằng bạn đang trả về biến không đồng bộ hóa, vì vậy biến thể hiện có thể không được xây dựng hoàn toàn (bạn sẽ thấy giá trị mặc định của các thuộc tính thay vì các giá trị được cung cấp trong phần tử này). constructor).

Biến boolean không thêm bất kỳ thứ gì để tránh vấn đề, vì nó có thể được đặt thành true trước khi lớp Test được khởi tạo (từ khóa được đồng bộ không tránh sắp xếp lại hoàn toàn, một số sencences có thể thay đổi thứ tự). Không có quy tắc nào xảy ra trước khi trong Mô hình bộ nhớ Java để đảm bảo điều đó.

Và làm cho biến động boolean sẽ không thêm bất cứ điều gì, bởi vì 32 bit biến được tạo ra nguyên tử trong Java. Các thành ngữ khóa kiểm tra đôi sẽ làm việc với họ là tốt.

Vì Java 5, bạn có thể khắc phục sự cố đó tuyên bố biến mẫu là biến động.

Bạn có thể đọc thêm về thành ngữ được chọn đôi trong this very interesting article.

Cuối cùng, một số khuyến nghị Tôi đã đọc:

  • Cân nhắc nếu bạn nên sử dụng Singleton pattern. Nó được coi là một mô hình chống nhiều người. Dependency Injection được ưu tiên khi có thể. Kiểm tra this.

  • Hãy cân nhắc kỹ nếu tối ưu hóa khóa đã kiểm tra kép thực sự cần thiết trước khi triển khai, bởi vì trong hầu hết các trường hợp, điều đó sẽ không đáng để thử. Ngoài ra, hãy xem xét việc xây dựng lớp Test trong trường tĩnh, vì việc tải lười chỉ hữu ích khi xây dựng một lớp có rất nhiều tài nguyên và trong hầu hết các lần, nó không phải là trường hợp.

Nếu bạn vẫn cần thực hiện tối ưu hóa này, hãy kiểm tra điều này link cung cấp một số lựa chọn thay thế để đạt được hiệu quả tương tự như những gì bạn đang cố gắng.

0

Sự cố DCL bị hỏng, mặc dù nó dường như hoạt động trên nhiều máy ảo. Có một cách viết đẹp về vấn đề ở đây http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html.

Đa luồng và kết nối bộ nhớ là những chủ đề phức tạp hơn chúng có thể xuất hiện. [...] Bạn có thể bỏ qua tất cả sự phức tạp này nếu bạn chỉ sử dụng công cụ mà Java cung cấp cho chính xác mục đích này - đồng bộ hóa. Nếu bạn đồng bộ hóa mọi quyền truy cập vào một biến có thể đã được viết hoặc có thể được đọc bởi, một chuỗi khác, bạn sẽ không gặp phải sự cố kết nối bộ nhớ.

Cách duy nhất để giải quyết vấn đề này đúng là tránh khởi tạo lười (làm điều đó háo hức) hoặc để kiểm tra đơn lẻ bên trong khối được đồng bộ hóa. Việc sử dụng boolean initialized tương đương với một kiểm tra null trên tham chiếu chính nó. Chuỗi thứ hai có thể thấy initialized là đúng nhưng instance vẫn có thể bị vô hiệu hoặc được khởi tạo một phần.

0

Khóa đôi được kiểm tra là kiểu chống.

Chủ sở hữu khởi tạo lười biếng Lớp là mẫu bạn nên xem.

Mặc dù có rất nhiều câu trả lời khác, tôi nghĩ rằng tôi nên trả lời vì vẫn không có một câu trả lời đơn giản nào nói tại sao DCL bị hỏng trong nhiều ngữ cảnh, tại sao nó không cần thiết và bạn nên làm gì. Vì vậy, tôi sẽ sử dụng một trích dẫn từ Goetz: Java Concurrency In Practice để tôi cung cấp giải thích succint nhất trong chương cuối cùng của nó trên Mô hình bộ nhớ Java.

Đó là về bản an toàn của các biến:

Vấn đề thực sự với DCL là giả định rằng điều tồi tệ nhất có thể xảy ra khi đọc một tài liệu tham khảo đối tượng chia sẻ mà không cần đồng bộ hóa là sai lầm thấy một giá trị cũ (trong này trường hợp, null); trong trường hợp đó, thành ngữ DCL đền bù cho rủi ro này bằng cách thử lại bằng khóa được giữ. Nhưng trường hợp xấu nhất thực sự tồi tệ hơn - có thể thấy giá trị hiện tại của tham chiếu nhưng giá trị cũ cho trạng thái của đối tượng, có nghĩa là đối tượng có thể được nhìn thấy ở trạng thái không hợp lệ hoặc không chính xác.

Các thay đổi tiếp theo trong JMM (Java 5.0 trở lên) đã cho phép DCL hoạt động nếu tài nguyên được tạo dễ bay hơi và tác động hiệu suất của điều này nhỏ vì đọc dễ bay hơi thường đắt hơn một chút so với lần đọc không linh hoạt. Tuy nhiên, đây là một thành ngữ mà phần lớn tiện ích của nó đã vượt qua — các lực lượng thúc đẩy nó (đồng bộ hóa chậm không được đồng bộ hóa, khởi động JVM chậm) không còn hoạt động nữa, làm cho nó kém hiệu quả hơn khi tối ưu hóa. Thành ngữ chủ khởi tạo lười biếng cung cấp các lợi ích giống nhau và dễ hiểu hơn.

Ví dụ 16.6. Lazy Chủ sở hữu khởi tạo Class Idiom.

public class ResourceFactory 
    private static class ResourceHolder { 
     public static Resource resource = new Resource(); 
    } 

    public static Resource getResource() { 
     return ResourceHolder.resource; 
    } 
} 

Đó là cách để làm điều đó.

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