2012-06-15 137 views
13

Ngày hôm khác Howard Lewis Ship đăng tải một entry trên blog gọi "Things I Learned at Hacker Bed and Breakfast", một trong những điểm viên đạn là:khởi Lazy mà không đồng bộ hoặc từ khóa volatile

Một Java instance field được gán đúng một lần qua lười biếng khởi không không cần phải đồng bộ hóa hoặc dễ bay hơi (miễn là vì bạn có thể chấp nhận các điều kiện chủng tộc qua các chủ đề để gán cho trường ); đây là từ Rich Hickey

Đối mặt với điều này, điều này có vẻ phù hợp với sự chấp nhận về khả năng hiển thị thay đổi bộ nhớ trên chủ đề và trong sách Java spec sau đó tôi đã bỏ lỡ nó. Nhưng đây là một cái gì đó mà HLS nhận được từ Rich Hickey tại một sự kiện mà Brian Goetz đã có mặt, nên có vẻ như nó phải có thứ gì đó với nó. Ai đó có thể vui lòng giải thích logic đằng sau tuyên bố này?

+0

đừng lo sợ đọc dễ bay hơi. khởi tạo lớp, tức là mã có thể sửa đổi là cách di động duy nhất để thực hiện điều đó với tính dễ bay hơi. Tuyên bố không chính xác khi đối mặt với kiến ​​trúc CPU cho phép viết lại được sắp xếp lại.Ngày x86 và Sparc TSO dễ đọc là miễn phí, vì vậy không có điểm để chơi một hacker. – bestsss

Trả lời

9

Tuyên bố này có vẻ hơi khó hiểu. Tuy nhiên, tôi đoán HLS đề cập đến trường hợp khi bạn lười biếng khởi tạo một trường thể hiện và không quan tâm nếu một số chủ đề thực hiện khởi tạo này nhiều hơn một lần.
Như một ví dụ, tôi có thể trỏ đến các phương pháp hashCode() của String lớp:

private int hashCode; 

public int hashCode() { 
    int hash = hashCode; 
    if (hash == 0) { 
     if (count == 0) { 
      return 0; 
     } 
     final int end = count + offset; 
     final char[] chars = value; 
     for (int i = offset; i < end; ++i) { 
      hash = 31*hash + chars[i]; 
     } 
     hashCode = hash; 
    } 
    return hash; 
} 

Như bạn có thể thấy quyền truy cập vào các hashCode lĩnh vực (trong đó giữ giá trị được lưu trữ của chuỗi tính toán hash) không được đồng bộ hóa và lĩnh vực này không được khai báo là volatile. Bất kỳ chuỗi nào gọi phương thức hashCode() sẽ vẫn nhận được cùng một giá trị, mặc dù trường hashCode có thể được viết nhiều lần bằng các chuỗi khác nhau.

Kỹ thuật này có khả năng sử dụng hạn chế. IMHO có thể sử dụng chủ yếu cho các trường hợp như trong ví dụ: một đối tượng nguyên gốc/không thay đổi được tính toán từ các trường cuối cùng/không thay đổi khác, nhưng tính toán của nó trong hàm tạo là quá mức cần thiết.

5

Edit:

HRM. Khi tôi đọc nó về mặt kỹ thuật là không chính xác nhưng được chấp nhận trong thực tế với một số cảnh báo. Chỉ các trường cuối cùng có thể được khởi tạo một cách an toàn một lần và được truy cập trong nhiều luồng mà không cần đồng bộ hóa.

Chủ đề khởi tạo lười biếng có thể gặp phải sự cố đồng bộ hóa theo một số cách. Ví dụ, bạn có thể có các điều kiện chạy thi công khi tham chiếu của lớp đã được xuất mà không có chính lớp đó được khởi tạo đầy đủ.

Tôi nghĩ rằng điều đó phụ thuộc rất lớn vào việc bạn có trường gốc hay đối tượng hay không. Các trường nguyên thủy có thể được khởi tạo nhiều lần mà bạn không nhớ rằng nhiều luồng làm việc khởi tạo sẽ hoạt động tốt. Tuy nhiên HashMap khởi tạo kiểu theo cách này có thể có vấn đề. Ngay cả các giá trị long trên một số kiến ​​trúc có thể lưu trữ các từ khác nhau trong nhiều thao tác để có thể xuất một nửa giá trị mặc dù tôi nghi ngờ rằng long sẽ không bao giờ vượt qua một trang bộ nhớ.

Tôi nghĩ rằng nó phụ thuộc cao vào việc hoặc không phải là một ứng dụng có bất kỳ rào cản bộ nhớ - bất kỳ synchronized khối hoặc truy cập vào volatile lĩnh vực. Ma quỷ là chắc chắn trong các chi tiết ở đây và mã mà không khởi tạo lười biếng có thể làm việc tốt trên một kiến ​​trúc với một bộ mã và không phải trong một mô hình chủ đề khác nhau hoặc với một ứng dụng đồng bộ hiếm khi.


Dưới đây là một mảnh tốt về lĩnh vực cuối cùng như một so sánh:

http://www.javamex.com/tutorials/synchronization_final.shtml

Tính đến Java 5, một sử dụng cụ thể của từ khóa cuối cùng là một vũ khí rất quan trọng và thường bị bỏ qua trong kho vũ khí đồng thời của bạn. Về cơ bản, cuối cùng có thể được sử dụng để đảm bảo rằng khi bạn xây dựng một đối tượng, một luồng khác truy cập đối tượng đó không thấy đối tượng đó ở trạng thái được xây dựng một phần, như có thể xảy ra khác.Điều này là do khi được sử dụng làm thuộc tính trên các biến của một đối tượng, cuối cùng có đặc điểm quan trọng sau đây như một phần của định nghĩa của nó:

Bây giờ, ngay cả khi trường được đánh dấu cuối cùng, nếu đó là một lớp, bạn có thể sửa đổi các trường trong vòng lớp học. Đây là vấn đề khác và bạn vẫn phải đồng bộ hóa vấn đề này.

5

Tính năng này hoạt động tốt trong một số điều kiện.

  • không sao để thử và đặt trường nhiều lần.
  • không sao nếu chủ đề riêng lẻ thấy các giá trị khác nhau.

Thường khi bạn tạo một đối tượng không được thay đổi, ví dụ: tải một thuộc tính từ đĩa, có nhiều hơn một bản sao cho một khoảng thời gian ngắn không phải là một vấn đề.

private static Properties prop = null; 

public static Properties getProperties() { 
    if (prop == null) { 
     prop = new Properties(); 
     try { 
      prop.load(new FileReader("my.properties")); 
     } catch (IOException e) { 
      throw new AssertionError(e); 
     } 
    } 
    return prop; 
} 

Trong ngắn hạn, điều này kém hiệu quả hơn so với khóa, nhưng về lâu dài, điều này có thể hiệu quả hơn. (Mặc dù Properties có khóa riêng nhưng bạn có ý tưởng;)

IMHO, Đây không phải là giải pháp hoạt động trong mọi trường hợp.

Có lẽ vấn đề là bạn có thể sử dụng các kỹ thuật nhất quán về bộ nhớ thư giãn hơn trong một số trường hợp.

+2

Tuy nhiên, điều này gặp phải các vấn đề về điều kiện chạy thi công. Bạn có thể lấy tham chiếu đến đối tượng được xuất sang một luồng khác mà không có đối tượng nào được khởi tạo hoàn toàn. – Gray

+0

@Gray sự hiểu biết của tôi về khởi tạo lười biếng là nó luôn luôn khởi tạo theo yêu cầu. Không thể thấy giá trị không có vốn đầu tư, nhưng bạn có thể cố gắng đặt giá trị này nhiều lần. –

+0

Vấn đề như tôi thấy nó @Peter là nếu không có đồng bộ hóa, không có rào cản bộ nhớ thì có khả năng một phần của một đối tượng đang được chia sẻ giữa bộ nhớ cache mà không lưu trữ đối tượng _full_ đang được cập nhật. Nếu luồng A chạy trên một tiến trình khác có bản sao trang # 1 nhưng không phải trang # 2 và một đối tượng có lưu trữ trong mỗi trang bị lười khởi tạo bởi luồng B, thì luồng A có thể lấy đối tượng được khởi tạo một phần. – Gray

3

Tôi nghĩ rằng tuyên bố là không đúng sự thật. Một luồng khác có thể thấy một đối tượng được khởi tạo một phần, vì vậy tham chiếu có thể được hiển thị cho một luồng khác mặc dù hàm tạo chưa chạy xong. Điều này được đề cập trong Java Concurrency trong Thực tiễn, phần 3.5.1:

public class Holder { 

    private int n; 

    public Holder (int n) { this.n = n; } 

    public void assertSanity() { 
     if (n != n) 
      throw new AssertionError("This statement is false."); 
    } 

} 

Lớp này không an toàn chỉ.

Nếu đối tượng hiển thị là bất biến, thì tôi không sao, vì ngữ nghĩa của trường cuối cùng có nghĩa là bạn sẽ không thấy chúng cho đến khi hàm tạo của nó chạy xong (mục 3.5.2).

+0

+1 để tham khảo :-) – dacwe

+0

@ dacwe Tôi yêu cầu upvote của tôi để gõ tất cả những gì trong :-) – artbristol

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