2012-06-12 31 views
14

Chúng ta có nên khai báo các trường riêng tư là volatile nếu instanced được sử dụng trong nhiều luồng không?java: các trường riêng tư `dễ bay hơi với getters và setters

Trong Effective Java, có một ví dụ trong đó mã số không làm việc mà không dễ bay hơi:

import java.util.concurrent.TimeUnit; 

// Broken! - How long would you expect this program to run? 
public class StopThread { 
    private static boolean stopRequested; // works, if volatile is here 

    public static void main(String[] args) throws InterruptedException { 
     Thread backgroundThread = new Thread(new Runnable() { 
      public void run() { 
       int i = 0; 
       while (!stopRequested) 
        i++; 
      } 
     }); 
     backgroundThread.start(); 
     TimeUnit.SECONDS.sleep(1); 
     stopRequested = true; 
    } 
} 

Các giải thích nói rằng

while(!stopRequested) 
    i++; 

được tối ưu hóa để một cái gì đó như thế này:

if(!stopRequested) 
    while(true) 
     i++; 

sửa đổi thêm nữa của stopRequested không được nhìn thấy theo chủ đề nền, vì vậy nó lặp đi lặp lại mãi mãi. (BTW, mã mà chấm dứt mà không volatile trên JRE7.)

Bây giờ xem xét lớp này:

public class Bean { 
    private boolean field = true; 

    public boolean getField() { 
     return field; 
    } 

    public void setField(boolean value) { 
     field = value; 
    } 
} 

và một sợi như sau:

public class Worker implements Runnable { 
    private Bean b; 

    public Worker(Bean b) { 
     this.b = b; 
    } 

    @Override 
    public void run() { 
     while(b.getField()) { 
      System.err.println("Waiting..."); 
      try { Thread.sleep(1000); } 
      catch(InterruptedException ie) { return; } 
     } 
    } 
} 

Đoạn mã trên làm việc như mong đợi mà không sử dụng các chất bay hơi :

public class VolatileTest { 
    public static void main(String [] args) throws Exception { 
     Bean b = new Bean(); 

     Thread t = new Thread(new Worker(b)); 
     t.start(); 
     Thread.sleep(3000); 

     b.setField(false); // stops the child thread 
     System.err.println("Waiting the child thread to quit"); 
     t.join(); 
     // if the code gets, here the child thread is stopped 
     // and it really gets, with JRE7, 6 with -server, -client 
    } 
} 

Tôi nghĩ vì công khai setter, c ompiler/JVM không bao giờ nên tối ưu hóa mã gọi getField(), nhưng this article nói rằng có một số mẫu "Volatile Bean" (Mẫu # 4), nên được áp dụng để tạo các lớp an toàn có thể thay đổi được. Cập nhật: có lẽ bài viết đó chỉ áp dụng cho IBM JVM?

Câu hỏi đặt ra là: phần nào của JLS nói rõ ràng hoặc ngầm định rằng các trường nguyên thủy riêng với getters/setters công khai phải được khai báo là volatile (hoặc chúng không phải)?

Xin lỗi một câu hỏi dài, tôi đã cố gắng giải thích sự cố chi tiết. Hãy cho tôi biết nếu có điều gì đó không rõ ràng. Cảm ơn.

+0

không giống như bạn cần trường đó để hủy chuỗi, bạn có thể sử dụng cờ bị gián đoạn trên chuỗi. –

+1

@NathanHughes, các lớp này chỉ là các ví dụ tối thiểu, mã thực tế là khác nhau, không có sự gián đoạn luồng là cần thiết ở đó. – khachik

Trả lời

4

Trước khi tôi trả lời câu hỏi của bạn tôi muốn giải quyết

BTW, mã mà chấm dứt mà không dễ bay hơi trên JRE7

này có thể thay đổi nếu bạn đã triển khai các ứng dụng tương tự với các đối số thời gian chạy khác nhau. Hoisting không nhất thiết phải là một cài đặt mặc định cho các JVM để nó có thể làm việc trong một và không hoạt động với nhau.

Để trả lời câu hỏi của bạn không có gì ngăn cản các trình biên dịch Java từ thực hiện ví dụ sau của bạn như là để

@Override 
public void run() { 
    if(b.getField()){ 
     while(true) { 
      System.err.println("Waiting..."); 
      try { Thread.sleep(1000); } 
      catch(InterruptedException ie) { return; } 
     } 
    } 
} 

Nó vẫn là tuần tự phù hợp và do đó duy trì bảo đảm Java - bạn có thể đọc đặc biệt 17.4.3:

Trong số tất cả các hoạt động liên thread được thực hiện bởi mỗi chủ đề t, thứ tự chương trình là t là tổng số thứ tự phản ánh thứ tự trong đó những hành động này sẽ được thực hiện theo chủ đề nội tuyến ngữ nghĩa của t.

Một tập hợp các hành động là tuần tự phù hợp nếu tất cả các hành động xảy ra trong một tổng trật tự (thứ tự thực hiện) đó là phù hợp với chương trình trật tự, và hơn nữa, mỗi đọc r của một v biến thấy giá trị được viết bởi các viết từ w đến v sao cho:

Nói cách khác - Miễn là một chuỗi sẽ đọc và viết một trường theo cùng thứ tự bất kể trình tự/bộ nhớ được đặt hàng được coi là liên tục tuần tự.

9

Câu hỏi đặt ra là: đó là một phần của JLS rõ ràng hoặc ngầm nói rằng lĩnh vực nguyên thủy riêng với thu khí công/setters phải được khai báo là dễ bay hơi (hoặc họ không phải)?

Mô hình bộ nhớ JLS không quan tâm đến getters/setters. Chúng không có gì từ quan điểm của mô hình bộ nhớ - bạn cũng có thể truy cập vào các trường công khai. Việc gói boolean phía sau một cuộc gọi phương thức không ảnh hưởng đến khả năng hiển thị bộ nhớ của nó. Ví dụ sau của bạn hoạt động hoàn toàn bằng may mắn.

Chúng tôi có nên khai báo các trường riêng tư dễ bay hơi nếu được sử dụng trong nhiều chủ đề không?

Nếu một lớp (bean) được sử dụng trong môi trường đa luồng, bạn phải bằng cách nào đó tính đến điều đó. Làm cho các trường riêng tư volatile là một cách tiếp cận: nó đảm bảo rằng mỗi luồng được đảm bảo để xem giá trị mới nhất của trường đó, không phải bất kỳ thứ gì được lưu trong bộ nhớ cache/tối ưu hóa các giá trị cũ. Nhưng nó không giải quyết được vấn đề của atomicity.

The article you linked to áp dụng cho bất kỳ JVM nào tuân theo đặc điểm JVM (mà JLS dựa vào). Bạn sẽ nhận được các kết quả khác nhau tùy thuộc vào nhà cung cấp JVM, phiên bản, cờ, máy tính và hệ điều hành, số lần bạn chạy chương trình (tối ưu hóa HotSpot thường khởi động sau lần chạy thứ 10000), vì vậy bạn thực sự phải hiểu thông số kỹ thuật và tuân thủ cẩn thận với các quy tắc để tạo ra các chương trình đáng tin cậy. Thử nghiệm trong trường hợp này là một cách không tốt để tìm hiểu cách mọi thứ hoạt động vì JVM có thể hoạt động theo bất kỳ cách nào nó muốn trong thời gian dài, và hầu hết các JVM đều chứa tất cả các loại tối ưu hóa động.

+0

cảm ơn bạn. Thiết lập một biến boolean (hoặc bất kỳ biến nguyên thủy nào khác ngoại trừ long/double) là một hoạt động nguyên tử. Câu hỏi của tôi là về cách JSL định nghĩa hành vi mã trong trường hợp này. Hoặc làm thế nào nó không xác định, nếu nó là rõ ràng những gì tôi có ý nghĩa. – khachik

+0

JLS không xác định bất kỳ điều gì cụ thể trong trường hợp của bạn. Một bean chỉ là các biến đằng sau các cuộc gọi phương thức, [mô hình bộ nhớ] (http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4) áp dụng như nó. Các cuộc gọi phương thức là không có-ops từ phối cảnh mô hình bộ nhớ. Bởi nguyên tử tôi có nghĩa là đậu của bạn có thể kết thúc ở trạng thái không nhất quán, nếu bạn có các lĩnh vực phụ thuộc lẫn nhau: ví dụ, thời gian "bắt đầu" phải ở trước thời gian "kết thúc" hoặc tương tự - nó không được đảm bảo trừ khi bạn có một cơ chế để thay đổi hai trường đó một cách nguyên tử. –

+0

Bằng cách này, việc viết và đọc các giá trị 'bay hơi'' dài' và 'đôi' luôn luôn là nguyên tử. Xem JLS 17.7. –

4

Không, mã đó không đúng. Không có gì trong JLS cho biết một trường phải được khai báo là dễ bay hơi. Tuy nhiên, nếu bạn muốn mã của bạn hoạt động chính xác trong môi trường nhiều luồng, thì bạn phải tuân thủ các quy tắc hiển thị. dễ bay hơi và đồng bộ là hai trong số các phương tiện chính để tạo dữ liệu chính xác trên các chủ đề.

Ví dụ của bạn, khó khăn khi viết mã đa luồng là nhiều hình thức mã không chính xác hoạt động tốt trong thử nghiệm. Chỉ vì thử nghiệm đa luồng "thành công" trong thử nghiệm không có nghĩa là mã đúng.

Để biết tham chiếu JLS cụ thể, hãy xem phần Happens Before (và phần còn lại của trang).

Lưu ý, theo nguyên tắc chung, nếu bạn nghĩ bạn đã tìm ra một cách mới thông minh để tìm kiếm các thành ngữ an toàn "chuẩn" an toàn, bạn có nhiều khả năng sai.

+0

nếu không có gì trong JSL nói rằng trường đó phải được khai báo là dễ bay hơi, làm sao bạn biết rằng nó phải được khai báo dễ bay hơi? Ý kiến ​​của bạn về việc thử nghiệm mã chuỗi đột biến là nhiều hơn OK, nhưng câu hỏi là về JSL. – khachik

+0

@khachik - JLS cung cấp các chi tiết cụ thể về những gì bạn phải làm để đảm bảo an toàn cho chuỗi của bạn. có nhiều tùy chọn trong phần tôi đã tham chiếu. không có gì trong JLS _requires_ bạn để làm cho chuỗi của bạn an toàn, do đó bạn không phải làm cho trường biến động (nếu không thì tất cả các trường sẽ biến động theo mặc định). – jtahlborn

+0

chắc chắn. Nó không trả lời câu hỏi của tôi. – khachik