2010-09-23 20 views
5

Có điều gì sai với sự an toàn của chuỗi java này không? Chủ đề 1-10 thêm số qua sample.add(), và Chủ đề 11-20 gọi removeAndDouble() và in kết quả thành stdout. Tôi nhớ lại từ phía sau trong tâm trí của tôi rằng ai đó nói rằng chỉ định mục theo cùng một cách như tôi đã có trong removeAndDouble() bằng cách sử dụng nó bên ngoài khối đồng bộ có thể không được thread an toàn. Trình biên dịch có thể tối ưu hóa các hướng dẫn để chúng xuất hiện ngoài trình tự. Đó có phải là trường hợp ở đây không? Phương thức removeAndDouble() của tôi có an toàn không?Gán một đối tượng cho một trường được xác định bên ngoài một khối được đồng bộ hóa - nó có phải là chủ đề an toàn không?

Có điều gì khác không đúng với phối cảnh đồng thời với mã này không? Tôi đang cố gắng để có được một sự hiểu biết tốt hơn về concurrency và mô hình bộ nhớ với java (1.6 trở lên).

import java.util.*; 
import java.util.concurrent.*; 

public class Sample { 

    private final List<Integer> list = new ArrayList<Integer>(); 

    public void add(Integer o) { 
     synchronized (list) { 
      list.add(o); 
      list.notify(); 
     } 
    } 

    public void waitUntilEmpty() { 
     synchronized (list) { 
      while (!list.isEmpty()) { 
       try { 
        list.wait(10000); 
       } catch (InterruptedException ex) { } 
      } 
     } 
    } 

    public void waitUntilNotEmpty() { 
     synchronized (list) { 
      while (list.isEmpty()) { 
       try { 
        list.wait(10000); 
       } catch (InterruptedException ex) { } 
      } 
     } 
    } 

    public Integer removeAndDouble() { 
     // item declared outside synchronized block 
     Integer item; 
     synchronized (list) { 
      waitUntilNotEmpty(); 
      item = list.remove(0); 
     } 
     // Would this ever be anything but that from list.remove(0)? 
     return Integer.valueOf(item.intValue() * 2); 
    } 

    public static void main(String[] args) { 
     final Sample sample = new Sample(); 

     for (int i = 0; i < 10; i++) { 
      Thread t = new Thread() { 
       public void run() { 
        while (true) { 
         System.out.println(getName()+" Found: " + sample.removeAndDouble()); 
        } 
       } 
      }; 
      t.setName("Consumer-"+i); 
      t.setDaemon(true); 
      t.start(); 
     } 

     final ExecutorService producers = Executors.newFixedThreadPool(10); 
     for (int i = 0; i < 10; i++) { 
      final int j = i * 10000; 
      Thread t = new Thread() { 
       public void run() { 
        for (int c = 0; c < 1000; c++) { 
         sample.add(j + c); 
        } 
       } 
      }; 
      t.setName("Producer-"+i); 
      t.setDaemon(false); 
      producers.execute(t); 
     } 

     producers.shutdown(); 
     try { 
      producers.awaitTermination(600, TimeUnit.SECONDS); 
     } catch (InterruptedException e) { 
      e.printStackTrace(); 
     } 

     sample.waitUntilEmpty();   
     System.out.println("Done."); 
    } 
} 

Trả lời

6

Đồng bộ hóa của bạn ổn, và sẽ không dẫn đến bất kỳ sự cố thực hiện không theo yêu cầu nào.

Tuy nhiên, tôi nhận thấy một số vấn đề.

Thứ nhất, phương pháp waitUntilEmpty của bạn sẽ được nhiều hơn nữa kịp thời nếu bạn thêm một list.notifyAll() sau list.remove(0) trong removeAndDouble. Điều này sẽ loại bỏ thời gian trễ tối đa 10 giây trong số wait(10000) của bạn.

Thứ hai, bạn list.notify trong phải là một notifyAll, vì notify chỉ tỉnh một chủ đề, và nó có thể đánh thức một thread đang chờ đợi bên waitUntilEmpty thay vì waitUntilNotEmpty. Thứ ba, không có điều nào ở trên là thiết bị đầu cuối cho tính linh hoạt của ứng dụng, bởi vì bạn đã sử dụng các lần chờ đợi, nhưng nếu bạn thực hiện hai thay đổi trên, ứng dụng của bạn sẽ có hiệu suất luồng tốt hơn (waitUntilEmpty) và chờ đợi bị ràng buộc trở nên không cần thiết và có thể trở thành đồng bằng cũ no-arg chờ đợi.

+0

Cuộc gọi tốt trên 'notifyAll'. –

+0

Có cuộc gọi tốt trên notifyAll() và thời gian chờ chờ. – Mike

8

Dường như chủ đề an toàn với tôi. Đây là lý do của tôi.

Mỗi lần bạn truy cập list bạn thực hiện đồng bộ hóa. Điều đó thật tuyệt. Mặc dù bạn rút ra một phần của list trong item, rằng item không được truy cập bởi nhiều luồng.

Chừng nào bạn chỉ truy cập list trong khi đồng bộ hóa, bạn nên được tốt (trong thiết kế hiện tại của bạn.)

3

Mã của bạn như nó vốn có trên thực tế là chủ đề an toàn. Lý do đằng sau điều này là hai phần.

Loại trừ đầu tiên là loại trừ lẫn nhau. Đồng bộ hóa của bạn chính xác đảm bảo rằng chỉ có một luồng tại một thời điểm sẽ sửa đổi các bộ sưu tập.

Điều thứ hai liên quan đến mối lo ngại của bạn về sắp xếp lại trình biên dịch. Bạn đang lo lắng rằng các biên dịch có thể trong thực tế lại thứ tự gán, trong đó nó sẽ không được thread an toàn. Bạn không phải lo lắng về nó trong trường hợp này. Đồng bộ hóa trên danh sách sẽ tạo ra một mối quan hệ xảy ra trước đó. Tất cả các loại bỏ khỏi danh sách xảy ra trước khi ghi vào Integer item. Điều này cho trình biên dịch biết rằng nó không thể ra lệnh ghi vào mục trong phương thức đó.

1

Mã của bạn an toàn chỉ nhưng không đồng thời (như song song). Như tất cả mọi thứ được truy cập dưới một khóa loại trừ lẫn nhau duy nhất, bạn đang serialising tất cả các truy cập, trong truy cập có hiệu lực đến cấu trúc là đơn luồng.

Nếu bạn yêu cầu chức năng như được mô tả trong mã sản xuất của mình, gói java.util.concurrent đã cung cấp BlockingQueue với mảng (kích thước cố định) và triển khai dựa trên danh sách được liên kết có thể phát triển. Đây là những điều rất thú vị để nghiên cứu các ý tưởng thực hiện ít nhất.

+0

Tôi không sắp xếp toàn bộ quyền truy cập - chỉ cần truy cập vào hàng đợi. Các nhà sản xuất và người tiêu dùng là song song (và trong một kịch bản điển hình sẽ tiêu thụ nhiều thời gian xử lý hơn so với ví dụ tầm thường ở trên). – Mike

+0

Vâng, tôi có nghĩa là serializing tất cả các truy cập vào cấu trúc hàng đợi :-) Ví dụ LinkedBlockingQueue có một khóa để đặt và khóa để lấy, làm cho nó ít có khả năng sản xuất và người tiêu dùng nối tiếp với nhau (hai nhà sản xuất serialize mặc dù). –

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