Các loại khác nhau của Bộ an toàn luồng trong Java


135

Dường như có rất nhiều cách triển khai và cách khác nhau để tạo Bộ an toàn luồng trong Java. Một số ví dụ bao gồm

1) CopyOnWriteArraySet

2) Bộ sưu tập. Bộ đồng bộ (Bộ đã đặt)

3) Đồng thờiSkipListSet

4) Bộ sưu tập.newSetFromMap (new concảnHashMap ())

5) Các bộ khác được tạo theo cách tương tự (4)

Các ví dụ này đến từ Mẫu tương tranh: Cài đặt đồng thời trong Java 6

Ai đó có thể vui lòng giải thích đơn giản về sự khác biệt, ưu điểm và nhược điểm của những ví dụ này và những ví dụ khác không? Tôi gặp khó khăn trong việc hiểu và giữ thẳng mọi thứ từ Tài liệu Std Java.

Câu trả lời:


206

1) Việc CopyOnWriteArraySettriển khai khá đơn giản - về cơ bản nó có một danh sách các phần tử trong một mảng và khi thay đổi danh sách, nó sao chép mảng. Lặp lại và các truy cập khác đang chạy tại thời điểm này tiếp tục với mảng cũ, tránh sự cần thiết phải đồng bộ hóa giữa người đọc và người viết (mặc dù bản thân văn bản cần phải được đồng bộ hóa). Các hoạt động thiết lập nhanh thông thường (đặc biệt contains()) khá chậm ở đây, vì các mảng sẽ được tìm kiếm trong thời gian tuyến tính.

Chỉ sử dụng điều này cho các bộ thực sự nhỏ sẽ được đọc (lặp lại) thường xuyên và hiếm khi thay đổi. (Bộ nghe nhạc Swings sẽ là một ví dụ, nhưng đây không phải là bộ thực sự và chỉ nên được sử dụng từ EDT.)

2) Collections.synchronizedSetsẽ chỉ đơn giản là bọc một khối đồng bộ hóa xung quanh mỗi phương thức của tập gốc. Bạn không nên truy cập trực tiếp vào bộ gốc. Điều này có nghĩa là không có hai phương thức nào của tập hợp có thể được thực thi đồng thời (một phương thức sẽ chặn cho đến khi kết thúc khác) - đây là an toàn luồng, nhưng bạn sẽ không có sự tương tranh nếu nhiều luồng thực sự sử dụng tập hợp. Nếu bạn sử dụng iterator, bạn vẫn cần phải đồng bộ hóa bên ngoài để tránh ConcurrencyModificationExceptions khi sửa đổi tập hợp giữa các lệnh gọi iterator. Hiệu suất sẽ giống như hiệu suất của bộ gốc (nhưng có một số chi phí đồng bộ hóa và chặn nếu được sử dụng đồng thời).

Sử dụng điều này nếu bạn chỉ có mức độ đồng thời thấp và muốn chắc chắn tất cả các thay đổi sẽ hiển thị ngay lập tức cho các luồng khác.

3) ConcurrentSkipListSetlà việc SortedSetthực hiện đồng thời , với hầu hết các hoạt động cơ bản trong O (log n). Nó cho phép thêm / xóa đồng thời và đọc / lặp, trong đó phép lặp có thể hoặc không thể nói về những thay đổi kể từ khi trình lặp được tạo. Các hoạt động hàng loạt chỉ đơn giản là nhiều cuộc gọi đơn lẻ, và không phải là nguyên tử - các luồng khác chỉ có thể quan sát một số trong số chúng.

Rõ ràng bạn chỉ có thể sử dụng điều này nếu bạn có một số thứ tự tổng thể trên các yếu tố của bạn. Điều này trông giống như một ứng cử viên lý tưởng cho các tình huống đồng thời cao, cho các tập không quá lớn (vì O (log n)).

4) Đối với ConcurrentHashMap(và Tập hợp có nguồn gốc từ nó): Ở đây, hầu hết các tùy chọn cơ bản là (trung bình, nếu bạn có tốt và nhanh hashCode()) trong O (1) (nhưng có thể suy biến thành O (n)), như đối với HashMap / Hashset. Có một sự đồng thời hạn chế cho việc viết (bảng được phân vùng và quyền truy cập ghi sẽ được đồng bộ hóa trên phân vùng cần thiết), trong khi quyền truy cập đọc hoàn toàn đồng thời với chính nó và các luồng viết (nhưng có thể chưa thấy kết quả của các thay đổi hiện đang diễn ra bằng văn bản). Trình lặp có thể hoặc không thể thấy các thay đổi kể từ khi nó được tạo và các hoạt động hàng loạt không phải là nguyên tử. Thay đổi kích thước là chậm (như đối với HashMap / Hashset), do đó, hãy cố gắng tránh điều này bằng cách ước tính kích thước cần thiết khi tạo (và sử dụng thêm khoảng 1/3 số đó, vì nó thay đổi kích thước khi đầy 3/4).

Sử dụng công cụ này khi bạn có bộ lớn, hàm băm tốt (và nhanh) và có thể ước tính kích thước bộ và đồng thời cần thiết trước khi tạo bản đồ.

5) Có triển khai bản đồ đồng thời nào khác mà người ta có thể sử dụng ở đây không?


1
Chỉ cần điều chỉnh tầm nhìn trong 1), quá trình sao chép dữ liệu vào mảng mới phải được khóa bằng cách đồng bộ hóa. Do đó, CopyOnWriteArraySet không hoàn toàn tránh sự cần thiết phải đồng bộ hóa.
CaptainHastings

Trên ConcurrentHashMapbộ dựa "do đó cố gắng tránh điều này bằng cách ước tính kích thước cần thiết về sự sáng tạo." Kích thước bạn cung cấp cho bản đồ phải lớn hơn 33% so với ước tính của bạn (hoặc giá trị đã biết), do bộ thay đổi kích thước ở mức tải 75%. Tôi sử dụngexpectedSize + 4 / 3 + 1
Daren

@Daren Tôi đoán đầu tiên +có nghĩa là một *?
Paŭlo Ebermann

@ PaŭloEbermann Tất nhiên ... nó phải thếexpectedSize * 4 / 3 + 1
Daren

1
Đối với ConcurrentMap(hoặc HashMap) trong Java 8 nếu số lượng mục nhập ánh xạ vào cùng một nhóm đạt giá trị ngưỡng (tôi tin là 16) thì danh sách sẽ được thay đổi thành cây tìm kiếm nhị phân (cây đỏ đen được đặt trước) và trong trường hợp đó, hãy tìm kiếm thời gian sẽ O(lg n)và không O(n).
akhil_mittal

20

Có thể kết hợp contains()hiệu suất của HashSetcác thuộc tính liên quan đến tương tranh CopyOnWriteArraySetbằng cách sử dụng AtomicReference<Set>và thay thế toàn bộ tập hợp trên mỗi sửa đổi.

Bản phác thảo thực hiện:

public abstract class CopyOnWriteSet<E> implements Set<E> {

    private final AtomicReference<Set<E>> ref;

    protected CopyOnWriteSet( Collection<? extends E> c ) {
        ref = new AtomicReference<Set<E>>( new HashSet<E>( c ) );
    }

    @Override
    public boolean contains( Object o ) {
        return ref.get().contains( o );
    }

    @Override
    public boolean add( E e ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( current.contains( e ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.add( e );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

    @Override
    public boolean remove( Object o ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( !current.contains( o ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.remove( o );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

}

Thực tế AtomicReferenceđánh dấu giá trị biến động. Nó có nghĩa là nó đảm bảo không có luồng nào đang đọc dữ liệu cũ và happens-befoređảm bảo rằng mã không thể được sắp xếp lại bởi trình biên dịch. Nhưng nếu chỉ AtomicReferencesử dụng các phương thức get / set thì chúng ta thực sự đang đánh dấu biến của chúng ta biến động theo một cách ưa thích.
akhil_mittal

Câu trả lời này không thể được nâng cấp đủ vì (1) trừ khi tôi bỏ lỡ điều gì đó, nó sẽ hoạt động cho tất cả các loại bộ sưu tập (2) không có lớp nào khác cung cấp cách cập nhật nguyên tử toàn bộ bộ sưu tập cùng một lúc ... Điều này rất hữu ích .
Gili

Tôi đã cố gắng chiếm đoạt nguyên văn này nhưng thấy nó được dán nhãn abstract, dường như để tránh phải viết một số phương pháp. Tôi bắt đầu thêm chúng vào, nhưng chạy vào một rào cản với iterator(). Tôi không biết làm thế nào để duy trì một trình vòng lặp cho điều này mà không phá vỡ mô hình. Có vẻ như tôi luôn phải trải qua refvà có thể nhận được một tập hợp cơ bản khác nhau mỗi lần, điều này đòi hỏi phải có một trình vòng lặp mới trên tập bên dưới, điều này vô dụng với tôi, vì nó sẽ bắt đầu với mục 0. Bất kỳ hiểu biết?
nclark

Được rồi tôi đoán đảm bảo là, mỗi khách hàng nhận được một ảnh chụp nhanh cố định, do đó, trình lặp của bộ sưu tập cơ bản sẽ hoạt động tốt nếu đó là tất cả những gì bạn cần. Trường hợp sử dụng của tôi là cho phép các luồng cạnh tranh "yêu cầu" các tài nguyên riêng lẻ trong đó và nó sẽ không hoạt động nếu chúng có các phiên bản khác nhau của tập hợp. Vào lần thứ hai mặc dù ... Tôi đoán chủ đề của tôi chỉ cần có một trình vòng lặp mới và thử lại nếu CopyOnWriteSet.remove (chọn_item) trả về sai ... Điều đó sẽ phải làm bất kể :)
nclark

11

Nếu Javadocs không giúp đỡ, có lẽ bạn chỉ nên tìm một cuốn sách hoặc bài viết để đọc về cấu trúc dữ liệu. Trong nháy mắt:

  • CopyOnWriteArraySet tạo một bản sao mới của mảng bên dưới mỗi khi bạn thay đổi bộ sưu tập, do đó việc ghi chậm và Iterators rất nhanh và nhất quán.
  • Bộ sưu tập.synyncizedset () sử dụng các cuộc gọi phương thức được đồng bộ hóa ở trường cũ để tạo một chuỗi chủ đề an toàn. Đây sẽ là một phiên bản hiệu suất thấp.
  • ConcurrencySkipListSet cung cấp ghi hiệu suất với các hoạt động hàng loạt không nhất quán (addAll, removeAll, v.v.) và Iterators.
  • Bộ sưu tập.

1
developer.com/java/article.php/10922_3829891_2/ từ <thậm chí còn tốt hơn một cuốn sách)
ycomp

1

Tập hợp các tài liệu tham khảo yếu

Một twist khác là một tập hợp các tài liệu tham khảo yếu an toàn .

Một bộ như vậy rất tiện cho việc theo dõi các thuê bao trong kịch bản pub-sub . Khi một thuê bao đi ra khỏi phạm vi ở những nơi khác, và do đó hướng tới việc trở thành một ứng cử viên cho việc thu gom rác, thuê bao không cần phải bận tâm đến việc hủy đăng ký một cách duyên dáng. Tham chiếu yếu cho phép người đăng ký hoàn thành quá trình chuyển đổi để trở thành ứng cử viên cho việc thu gom rác. Khi rác cuối cùng được thu thập, mục trong bộ được loại bỏ.

Mặc dù không có bộ nào được cung cấp trực tiếp với các lớp được đóng gói, bạn có thể tạo một lớp với một vài cuộc gọi.

Đầu tiên chúng ta bắt đầu với việc tạo một Settài liệu tham khảo yếu bằng cách tận dụng WeakHashMaplớp. Điều này được thể hiện trong tài liệu lớp cho Collections.newSetFromMap.

Set< YourClassGoesHere > weakHashSet = 
    Collections
    .newSetFromMap(
        new WeakHashMap< YourClassGoesHere , Boolean >()
    )
;

Các giá trị của bản đồ, Boolean, là không thích hợp ở đây là chính của bản đồ chiếm của chúng tôi Set.

Trong một kịch bản như pub-sub, chúng tôi cần an toàn luồng nếu người đăng ký và nhà xuất bản đang hoạt động trên các luồng riêng biệt (rất có thể là trường hợp này).

Đi thêm một bước nữa bằng cách gói như một bộ được đồng bộ hóa để làm cho bộ này an toàn. Thức ăn vào một cuộc gọi đến Collections.synchronizedSet.

this.subscribers =
        Collections.synchronizedSet(
                Collections.newSetFromMap(
                        new WeakHashMap <>()  // Parameterized types `< YourClassGoesHere , Boolean >` are inferred, no need to specify.
                )
        );

Bây giờ chúng tôi có thể thêm và xóa người đăng ký khỏi kết quả của chúng tôi Set. Và bất kỳ người đăng ký nào biến mất trên mạng cuối cùng sẽ bị xóa tự động sau khi thực hiện thu gom rác. Khi việc thực thi này xảy ra tùy thuộc vào việc triển khai trình thu gom rác của JVM của bạn và phụ thuộc vào tình huống thời gian chạy tại thời điểm này. Để thảo luận và ví dụ về thời điểm và cách thức bên dưới WeakHashMapxóa các mục đã hết hạn, hãy xem Câu hỏi này, * WeakHashMap có phát triển không, hay nó có xóa các khóa rác không? * .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.