2010-09-30 41 views
5

trong cố gắng để cải thiện sự hiểu biết của tôi về vấn đề đồng thời, tôi đang xem xét kịch bản sau đây (Edit: Tôi đã thay đổi ví dụ từ sách để Runtime, đó là gần gũi hơn với những gì tôi đang cố gắng):Xác định phạm vi đồng bộ hóa?

public class Example { 
    private final Object lock = new Object(); 
    private final Runtime runtime = Runtime.getRuntime(); 
    public void add(Object o) { 
     synchronized (lock) { runtime.exec(program + " -add "+o); } 
    } 
    public Object[] getAll() { 
     synchronized (lock) { return runtime.exec(program + " -list "); } 
    } 
    public void remove(Object o) { 
     synchronized (lock) { runtime.exec(program + " -remove "+o); } 
    } 
} 

Vì nó là viết tắt, mỗi phương pháp là bởi thread an toàn khi được sử dụng độc lập. Bây giờ, những gì tôi đang cố gắng tìm ra là làm thế nào để xử lý nơi lớp gọi điện thoại muốn gọi:

for (Object o : example.getAll()) { 
    // problems if multiple threads perform this operation concurrently 
    example.remove(b); 
} 

Nhưng như đã nói, không có gì bảo đảm rằng nhà nước sẽ được thống nhất giữa các cuộc gọi đến GetAll() và các cuộc gọi để xóa(). Nếu nhiều chủ đề gọi điều này, tôi sẽ gặp rắc rối. Vì vậy, câu hỏi của tôi là - Làm cách nào để tôi cho phép nhà phát triển thực hiện thao tác theo cách an toàn? Lý tưởng nhất là tôi muốn thực thi an toàn luồng theo cách gây khó khăn cho nhà phát triển để tránh/bỏ lỡ, nhưng đồng thời không phức tạp để đạt được. Tôi có thể nghĩ đến ba tùy chọn cho đến thời điểm này:

A: Tạo khóa 'this', vì vậy đối tượng đồng bộ hóa có thể truy cập vào mã gọi, sau đó có thể bọc các khối mã. Nhược điểm: Khó thực thi tại thời gian biên dịch:

synchronized (example) { 
    for (Object o : example.getAll()) { 
     example.remove(b); 
    } 
} 

B: Đặt mã kết hợp vào lớp Example - và có lợi để tối ưu hóa việc triển khai, như trong trường hợp này. Nhược điểm: Đau để thêm tiện ích mở rộng và khả năng trộn logic không có liên quan:

public class Example { 
    ... 
    public void removeAll() { 
     synchronized (lock) { Runtime.exec(program + " -clear"); } 
    } 
} 

C: Cung cấp lớp đóng. Nhược điểm: Mã dư thừa, có khả năng quá hào phóng của một khối đồng bộ hóa, thực tế có thể làm cho deadlocks dễ dàng hơn:

public interface ExampleClosure { 
    public void execute(Example example); 
} 
public Class Example { 
    ... 
    public void execute(ExampleClosure closure) { 
     synchronized (this) { closure.execute(this); } 
    } 
} 

example.execute(new ExampleClosure() { 
     public void execute(Example example) { 
      for (Object o : example.getAll()) { 
       example.remove(b); 
      } 
     } 
    } 
); 

Tôi đang thiếu cái gì? Làm thế nào đồng bộ hóa nên được scoped để đảm bảo mã là thread an toàn?

Trả lời

2

Tôi nghĩ mọi người đều thiếu vấn đề thực sự của mình. Khi lặp qua mảng mới của đối tượng và cố gắng loại bỏ một tại một thời điểm vấn đề vẫn còn kỹ thuật không an toàn (mặc dù ArrayList cấy sẽ không phát nổ, nó chỉ sẽ không có kết quả mong đợi).

Ngay cả với CopyOnWriteArrayList có khả năng là có một ngày đã đọc trong danh sách hiện tại khi bạn đang cố xóa.

Hai đề xuất bạn cung cấp là tốt (A và B). Đề nghị chung của tôi là B. Làm cho một bộ sưu tập thread-an toàn là rất khó khăn. Cách tốt nhất để làm điều đó là cung cấp cho khách hàng ít chức năng nhất có thể (trong phạm vi lý do). Vì vậy, việc cung cấp phương thức removeAll và loại bỏ phương thức getAll sẽ đủ.

Bây giờ bạn có thể đồng thời nói, 'tôi cũng muốn giữ API theo cách của nó và để cho khách hàng lo lắng về an toàn chủ đề bổ sung'. Nếu đó là trường hợp, tài liệu thread-an toàn. Ghi lại thực tế là hành động 'tra cứu và sửa đổi' không phải là nguyên tử và không an toàn.

Việc triển khai danh sách đồng thời của hôm nay là tất cả các chuỗi an toàn cho các chức năng đơn lẻ được cung cấp (nhận, loại bỏ thêm là tất cả các chuỗi an toàn). Chức năng hợp chất không phải là mặc dù và tốt nhất có thể được thực hiện là tài liệu làm thế nào để làm cho họ thread an toàn.

2

Tôi nghĩ j.u.c.CopyOnWriteArrayList là một ví dụ hay về vấn đề tương tự bạn đang cố giải quyết.

JDK có vấn đề tương tự với Danh sách - có nhiều cách khác nhau để đồng bộ hóa trên các phương pháp tùy ý, nhưng không đồng bộ hóa trên nhiều lời gọi (và điều đó dễ hiểu).

Vì vậy, CopyOnWriteArrayList thực sự triển khai cùng một giao diện nhưng có hợp đồng rất đặc biệt và bất kỳ ai gọi nó, đều biết điều đó.

Tương tự như giải pháp của bạn - bạn có thể triển khai List (hoặc bất kỳ giao diện nào) và đồng thời xác định hợp đồng đặc biệt cho các phương pháp hiện có/mới. Ví dụ: tính nhất quán của getAll không được đảm bảo và các cuộc gọi tới .remove sẽ không thành công nếu o không có hoặc không nằm trong danh sách, v.v. Nếu người dùng muốn cả tùy chọn kết hợp và an toàn/nhất quán - lớp này của bạn sẽ cung cấp một phương pháp đặc biệt thực hiện chính xác điều đó (ví dụ: safeDeleteAll), để lại các phương thức khác gần với hợp đồng gốc nhất có thể.

Vì vậy, để trả lời câu hỏi của bạn - tôi sẽ chọn tùy chọn B, nhưng cũng sẽ triển khai giao diện mà đối tượng ban đầu của bạn đang triển khai.

1

Từ Javadoc cho Danh sách.toArray():

Mảng được trả về sẽ "an toàn" trong mà không có tham chiếu đến nó là được duy trì trong danh sách này. (Trong các từ khác , phương pháp này phải phân bổ một mảng mới ngay cả khi danh sách này được hỗ trợ bởi một mảng). Người gọi được miễn phí để sửa đổi mảng được trả về.

Có thể tôi không hiểu những gì bạn đang cố gắng hoàn thành. Bạn có muốn mảng Object[] luôn đồng bộ hóa với trạng thái hiện tại của List không? Để đạt được điều đó, tôi nghĩ bạn sẽ phải đồng bộ hóa trên bản thân Example và giữ khóa cho đến khi chuỗi của bạn được thực hiện với cuộc gọi phương thức AND bất kỳ mảng Object[] hiện đang sử dụng. Nếu không, làm thế nào bạn sẽ biết nếu bản gốc List đã được sửa đổi bởi một chủ đề khác?

+0

Xin lỗi, ví dụ này có một chút không rõ ràng. Tôi đã xóa tham chiếu đến Danh sách. Tôi chỉ cố gắng tìm ra cách tốt nhất để giải quyết: Blah result = example.method1(); example.method2 (kết quả); trong đó lời gọi tới method2 có thể gây ra vấn đề bởi vì inbetween method1 và method2, một thread khác đã đi và được gọi là method3(). – Mike

3

Sử dụng ReentrantReadWriteLock được hiển thị qua API. Bằng cách đó, nếu ai đó cần phải đồng bộ hóa một số cuộc gọi API, họ có thể có được một khóa bên ngoài của các cuộc gọi phương thức.

+0

Không giống như tùy chọn A, sử dụng 'this' làm màn hình đồng bộ hóa? Tôi thấy lợi ích của đề xuất của bạn bằng cách làm cho nó khó khăn hơn để vô tình đồng bộ hóa trên nó mà không suy nghĩ. Trong trường hợp đó, sẽ: public Object getLock() {return ...; } có ý nghĩa hơn? – Mike

+0

Điều đó cũng có thể hoạt động vì các khóa được đồng bộ hóa cũng reentrant (cùng một luồng có thể nhận được cùng một khóa hai lần mà không bị chặn). Cách tiếp cận của tôi có lợi ích là một số luồng có thể gọi 'get()' cùng một lúc mà không bị chặn. –

1

Bạn phải sử dụng mức chi tiết thích hợp khi bạn chọn nội dung cần khóa. Những gì bạn đang phàn nàn trong ví dụ của bạn là mức độ chi tiết quá thấp, nơi khóa không bao gồm tất cả các phương thức phải xảy ra cùng nhau. Bạn cần phải thực hiện các phương pháp kết hợp tất cả các hành động cần phải xảy ra cùng nhau trong cùng một khóa.

Khóa được đặt lại để phương thức cấp cao có thể gọi các phương thức được đồng bộ hóa ở mức độ thấp mà không gặp sự cố.

+0

Có vẻ như một cách tiếp cận hợp lý, nơi tôi gặp khó khăn là phân định các phương pháp có logic nghiệp vụ được tập trung tốt nhất ở nơi khác: Trong trường hợp nhiều hành động cần xảy ra trong cùng một khóa, nhưng phức tạp hơn, ví dụ: không phải tất cả các mục đều bị xóa(), có lẽ chỉ có các mục phù hợp với một Predicate là? Tôi có nghĩ rằng đặt trong một 'công cộng void loại bỏ (Predicate p)' phương pháp trong lớp Ví dụ, hoặc tôi cho phép khóa được truy cập bằng mã gọi? – Mike

3

Nói chung, đây là vấn đề thiết kế đa luồng cổ điển. Bằng cách đồng bộ hóa cấu trúc dữ liệu thay vì đồng bộ hóa các khái niệm sử dụng cấu trúc dữ liệu, thật khó để tránh thực tế rằng bạn về cơ bản có tham chiếu đến cấu trúc dữ liệu mà không cần khóa.

Tôi khuyên bạn không nên khóa ổ khóa quá gần với cấu trúc dữ liệu. Nhưng đó là một lựa chọn phổ biến.

Một kỹ thuật tiềm năng để làm cho kiểu này hoạt động là sử dụng trình chỉnh sửa cây-walker. Về cơ bản, bạn trưng ra một hàm thực hiện gọi lại trên mỗi phần tử.

// pointer to function: 
//  - takes Object by reference and can be safely altered 
//  - if returns true, Object will be removed from list 

typedef bool (*callback_function)(Object *o); 

public void editAll(callback_function func) { 
    synchronized (lock) { 
      for each element o { if (callback_function(o)) {remove o} } } 
} 

Vì vậy, sau đó vòng lặp của bạn trở thành:

bool my_function(Object *o) { 
... 
    if (some condition) return true; 
} 

... 
    editAll(my_function); 
... 

Công ty tôi làm việc cho (corensic) có trường hợp thử nghiệm chiết xuất từ ​​lỗi thực để xác minh rằng Jinx là tìm các lỗi đồng thời đúng. Kiểu cấu trúc dữ liệu mức thấp này không có đồng bộ hóa mức cao hơn là kiểu khá phổ biến. Callback chỉnh sửa cây có vẻ là một sửa chữa phổ biến cho điều kiện chủng tộc này.

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