Berbagai jenis Set thread-safe di Java

138

Tampaknya ada banyak implementasi dan cara berbeda untuk menghasilkan Sets yang aman bagi thread di Java. Beberapa contohnya termasuk

1) CopyOnWriteArraySet

2) Collections.synchronizedSet (Set set)

3) ConcurrentSkipListSet

4) Collections.newSetFromMap (baru ConcurrentHashMap ())

5) Set Lain yang dihasilkan dengan cara yang mirip dengan (4)

Contoh ini berasal dari Concurrency Pattern: Concurrent Set implementasi di Java 6

Bisakah seseorang menjelaskan secara sederhana perbedaan, keuntungan, dan kerugian dari contoh-contoh ini dan lainnya? Saya mengalami masalah dalam memahami dan menyimpan semua hal dari Java Std Docs.

Ben
sumber

Jawaban:

206

1) Ini CopyOnWriteArraySetadalah implementasi yang cukup sederhana - pada dasarnya memiliki daftar elemen dalam array, dan ketika mengubah daftar, itu menyalin array. Iterasi dan akses lain yang sedang berjalan saat ini dilanjutkan dengan array lama, menghindari keharusan sinkronisasi antara pembaca dan penulis (meskipun penulisan itu sendiri perlu disinkronkan). Operasi set yang biasanya cepat (terutama contains()) cukup lambat di sini, karena array akan dicari dalam waktu linier.

Gunakan ini hanya untuk set yang sangat kecil yang akan sering dibaca (diulang) dan jarang diubah. (Swings listener-set akan menjadi contoh, tetapi ini sebenarnya bukan set, dan sebaiknya hanya digunakan dari EDT.)

2) Collections.synchronizedSethanya akan membungkus blok tersinkronisasi di sekitar setiap metode set asli. Anda tidak boleh mengakses set asli secara langsung. Ini berarti bahwa tidak ada dua metode set yang dapat dieksekusi secara bersamaan (satu akan memblokir hingga yang lain selesai) - ini aman untuk thread, tetapi Anda tidak akan memiliki konkurensi jika beberapa thread benar-benar menggunakan set. Jika Anda menggunakan iterator, Anda biasanya masih perlu melakukan sinkronisasi secara eksternal untuk menghindari ConcurrentModificationExceptions saat memodifikasi set antara panggilan iterator. Kinerja akan seperti kinerja set asli (tetapi dengan beberapa overhead sinkronisasi, dan pemblokiran jika digunakan secara bersamaan).

Gunakan ini jika Anda hanya memiliki konkurensi rendah, dan ingin memastikan semua perubahan langsung terlihat ke utas lainnya.

3) ConcurrentSkipListSetadalah SortedSetimplementasi bersamaan , dengan sebagian besar operasi dasar di O (log n). Ini memungkinkan penambahan / penghapusan dan pembacaan / iterasi secara bersamaan, di mana iterasi mungkin atau mungkin tidak memberi tahu tentang perubahan sejak iterator dibuat. Operasi massal hanyalah beberapa panggilan tunggal, dan tidak secara atomis - utas lain hanya dapat mengamati beberapa dari mereka.

Jelas Anda dapat menggunakan ini hanya jika Anda memiliki beberapa urutan total pada elemen Anda. Ini terlihat seperti kandidat ideal untuk situasi konkurensi tinggi, untuk set yang tidak terlalu besar (karena O (log n)).

4) Untuk ConcurrentHashMap(dan Set yang diturunkan darinya): Di sini sebagian besar opsi dasar adalah (rata-rata, jika Anda memiliki yang baik dan cepat hashCode()) di O (1) (tetapi mungkin merosot ke O (n)), seperti untuk HashMap / HashSet. Ada konkurensi terbatas untuk penulisan (tabel dipartisi, dan akses tulis akan disinkronkan pada partisi yang diperlukan), sementara akses baca sepenuhnya bersamaan dengan dirinya sendiri dan utas penulisan (tetapi mungkin belum melihat hasil dari perubahan yang saat ini sedang berlangsung. tertulis). Iterator mungkin atau mungkin tidak melihat perubahan sejak dibuat, dan operasi massal tidak bersifat atomik. Mengubah ukuran lambat (seperti untuk HashMap / HashSet), jadi cobalah untuk menghindari ini dengan memperkirakan ukuran yang dibutuhkan saat pembuatan (dan menggunakan sekitar 1/3 lebih dari itu, karena ukurannya berubah ketika 3/4 penuh).

Gunakan ini ketika Anda memiliki set besar, fungsi hash yang baik (dan cepat) dan dapat memperkirakan ukuran set dan konkurensi yang diperlukan sebelum membuat peta.

5) Apakah ada implementasi peta bersamaan lainnya yang dapat digunakan di sini?

Paŭlo Ebermann
sumber
1
Hanya koreksi penglihatan di 1), proses menyalin data ke dalam array baru harus dikunci dengan sinkronisasi. Oleh karena itu, CopyOnWriteArraySet tidak sepenuhnya menghindari kebutuhan sinkronisasi.
CaptainHastings
Pada ConcurrentHashMaphimpunan berbasis, "jadi cobalah untuk menghindari ini dengan memperkirakan ukuran yang dibutuhkan pada penciptaan." Ukuran yang Anda berikan pada peta harus lebih dari 33% lebih besar dari perkiraan Anda (atau nilai yang diketahui), karena ukuran set berubah pada beban 75%. Saya menggunakanexpectedSize + 4 / 3 + 1
Daren
@ Daren Saya kira yang pertama +dimaksudkan untuk menjadi *?
Paŭlo Ebermann
@ PaŭloEbermann Tentu saja ... itu harusexpectedSize * 4 / 3 + 1
Daren
1
Untuk ConcurrentMap(atau HashMap) di Java 8 jika jumlah entri yang dipetakan ke keranjang yang sama mencapai nilai ambang batas (saya yakin itu adalah 16) maka daftar tersebut akan diubah menjadi pohon pencarian biner (pohon merah-hitam untuk diutamakan) dan dalam hal ini cari waktu akan O(lg n)dan tidak O(n).
akhil_mittal
21

Dimungkinkan untuk menggabungkan contains()kinerja HashSetdengan properti terkait konkurensi CopyOnWriteArraySetdengan menggunakan AtomicReference<Set>dan mengganti seluruh set pada setiap modifikasi.

Sketsa implementasi:

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;
            }
        }
    }

}
Oleg Estekhin
sumber
Sebenarnya AtomicReferencemenandai nilai volatile. Ini berarti memastikan tidak ada utas yang membaca data lama dan memberikan happens-beforejaminan karena kode tidak dapat disusun ulang oleh kompilator. Tetapi jika hanya metode get / set AtomicReferenceyang digunakan maka kita sebenarnya menandai variabel kita volatile dengan cara yang mewah.
akhil_mittal
Jawaban ini tidak dapat cukup disukai karena (1) kecuali saya melewatkan sesuatu, ini akan berfungsi untuk semua jenis koleksi (2) tidak ada kelas lain yang menyediakan cara untuk memperbarui seluruh koleksi sekaligus ... Ini sangat berguna .
Gili
Saya mencoba menyesuaikan kata demi kata ini tetapi ternyata diberi label abstract, tampaknya untuk menghindari keharusan menulis beberapa metode. Saya mulai menambahkannya, tetapi mengalami hambatan dengan iterator(). Saya tidak tahu bagaimana mempertahankan iterator atas benda ini tanpa merusak model. Sepertinya saya selalu harus melalui ref, dan mungkin mendapatkan set dasar yang berbeda setiap kali, yang membutuhkan iterator baru pada set yang mendasarinya, yang tidak berguna bagi saya, karena akan dimulai dengan item nol. Ada wawasan?
nclark
Oke, saya kira jaminannya adalah, setiap pelanggan mendapatkan snapshot tetap tepat waktu, jadi iterator koleksi yang mendasarinya akan berfungsi dengan baik jika hanya itu yang Anda butuhkan. Kasus penggunaan saya adalah mengizinkan untaian yang bersaing untuk "mengklaim" sumber daya individu di dalamnya, dan itu tidak akan berfungsi jika mereka memiliki versi yang berbeda dari kumpulan. Namun kedua ... Saya kira utas saya hanya perlu mendapatkan iterator baru dan coba lagi jika CopyOnWriteSet.remove (selected_item) mengembalikan false ... Yang harus dilakukan terlepas dari :)
nclark
Apa while ( true )perlu di sini?
pengguna3908406
11

Jika Javadocs tidak membantu, Anda mungkin sebaiknya mencari buku atau artikel untuk membaca tentang struktur data. Sekilas:

  • CopyOnWriteArraySet membuat salinan baru dari larik yang mendasari setiap kali Anda memutasi koleksi, sehingga penulisan menjadi lambat dan Iterator cepat dan konsisten.
  • Collections.synchronizedSet () menggunakan panggilan metode tersinkronisasi sekolah lama untuk membuat Set threadsafe. Ini akan menjadi versi berkinerja rendah.
  • ConcurrentSkipListSet menawarkan penulisan performant dengan operasi batch yang tidak konsisten (addAll, removeAll, dll.) Dan Iterator.
  • Collections.newSetFromMap (new ConcurrentHashMap ()) memiliki semantik ConcurrentHashMap, yang menurut saya belum tentu dioptimalkan untuk membaca atau menulis, tetapi seperti ConcurrentSkipListSet, memiliki operasi batch yang tidak konsisten.
Ryan Stewart
sumber
1
developer.com/java/article.php/10922_3829891_2/… <bahkan lebih baik dari buku)
ycomp
1

Kumpulan referensi lemah serentak

Perubahan lain adalah serangkaian referensi lemah yang aman untuk thread .

Kumpulan seperti itu berguna untuk melacak pelanggan dalam skenario pub-sub . Saat pelanggan keluar dari ruang lingkup di tempat lain, dan karena itu menuju ke kandidat untuk pengumpulan sampah, pelanggan tidak perlu diganggu dengan berhenti berlangganan dengan anggun. Referensi lemah memungkinkan pelanggan menyelesaikan transisinya untuk menjadi kandidat pengumpulan sampah. Saat sampah akhirnya dikumpulkan, entri dalam kumpulan tersebut dihapus.

Meskipun tidak ada kumpulan tersebut yang secara langsung disediakan dengan kelas yang dibundel, Anda dapat membuatnya dengan beberapa panggilan.

Pertama kita mulai dengan membuat Setreferensi yang lemah dengan memanfaatkan WeakHashMapkelas. Ini diperlihatkan dalam dokumentasi kelas untuk Collections.newSetFromMap.

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

The Nilai dari peta, Boolean, tidak relevan di sini sebagai kunci dari peta membuat kami Set.

Dalam skenario seperti pub-sub, kita memerlukan keamanan thread jika pelanggan dan penerbit beroperasi pada thread terpisah (kemungkinan besar demikian).

Selangkah lebih maju dengan membungkus sebagai rangkaian yang disinkronkan untuk membuat rangkaian ini aman untuk thread. Masukkan panggilan ke Collections.synchronizedSet.

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

Sekarang kami dapat menambah dan menghapus pelanggan dari hasil kami Set. Dan setiap pelanggan yang "menghilang" pada akhirnya akan otomatis dihapus setelah pengumpulan sampah dijalankan. Kapan eksekusi ini terjadi bergantung pada implementasi pengumpul sampah JVM Anda, dan bergantung pada situasi waktu proses saat ini. Untuk diskusi dan contoh kapan dan bagaimana cara mendasari WeakHashMapmembersihkan entri kadaluwarsa, lihat Pertanyaan ini, * Apakah WeakHashMap terus berkembang, atau apakah itu menghapus kunci sampah? * .

Basil Bourque
sumber