Ada kasus di mana peta akan dibangun, dan begitu diinisialisasi, itu tidak akan pernah diubah lagi. Namun itu akan diakses (melalui get (kunci) saja) dari beberapa utas. Apakah aman untuk menggunakan java.util.HashMap
cara ini?
(Saat ini, saya dengan senang hati menggunakan java.util.concurrent.ConcurrentHashMap
, dan tidak memiliki kebutuhan yang terukur untuk meningkatkan kinerja, tetapi saya hanya ingin tahu apakah yang sederhana HashMap
sudah cukup. Oleh karena itu, pertanyaan ini bukan "Yang mana yang harus saya gunakan?" Juga bukan pertanyaan kinerja. Sebaliknya, pertanyaannya adalah "Apakah itu aman?")
java
multithreading
concurrency
hashmap
Dave L.
sumber
sumber
ClassLoader
. Itu layak pertanyaan terpisah sendiri. Saya masih akan menyinkronkannya secara eksplisit dan profil untuk memverifikasi bahwa itu menyebabkan masalah kinerja nyata.Map
, yang tidak Anda jelaskan. Jika Anda tidak melakukannya dengan aman, itu tidak aman. Jika Anda melakukannya dengan aman, itu benar . Detail dalam jawaban saya.Jawaban:
Idiom Anda aman jika dan hanya jika referensi ke
HashMap
yang aman diterbitkan . Daripada apa pun yang berkaitan dengan internalHashMap
itu sendiri, publikasi yang aman berkaitan dengan bagaimana utas pembuatan membuat referensi ke peta terlihat oleh utas lainnya.Pada dasarnya, satu-satunya ras yang mungkin ada di sini adalah antara konstruksi
HashMap
dan semua utas pembacaan yang dapat mengaksesnya sebelum sepenuhnya dibangun. Sebagian besar diskusi adalah tentang apa yang terjadi pada keadaan objek peta, tetapi ini tidak relevan karena Anda tidak pernah memodifikasinya - jadi satu-satunya bagian yang menarik adalah bagaimanaHashMap
referensi diterbitkan.Misalnya, bayangkan Anda menerbitkan peta seperti ini:
... dan pada titik tertentu
setMap()
disebut dengan peta, dan utas lainnya menggunakanSomeClass.MAP
untuk mengakses peta, dan periksa null seperti ini:Ini tidak aman meskipun mungkin tampak seperti itu. Masalahnya adalah bahwa tidak ada hubungan sebelum-terjadi antara himpunan
SomeObject.MAP
dan bacaan berikutnya pada utas lainnya, sehingga utas pembacaan bebas untuk melihat sebagian peta yang dibangun. Ini hampir dapat melakukan apa saja dan bahkan dalam praktiknya ia melakukan hal-hal seperti menempatkan utas membaca ke dalam lingkaran yang tak terbatas .Untuk menerbitkan peta dengan aman, Anda perlu membangun hubungan sebelum-terjadi antara penulisan referensi ke
HashMap
(yaitu, publikasi ) dan pembaca selanjutnya dari referensi itu (yaitu, konsumsi). Secara mudah, hanya ada beberapa cara yang mudah diingat untuk mencapai hal itu [1] :Yang paling menarik untuk skenario Anda adalah (2), (3) dan (4). Secara khusus, (3) berlaku langsung ke kode yang saya miliki di atas: jika Anda mengubah deklarasi
MAP
ke:maka semuanya menjadi halal: pembaca yang melihat nilai non-nol tentu memiliki hubungan yang terjadi sebelum dengan toko
MAP
dan karenanya melihat semua toko yang terkait dengan inisialisasi peta.Metode lain mengubah semantik metode Anda, karena keduanya (2) (menggunakan initalizer statis) dan (4) (menggunakan final ) menyiratkan bahwa Anda tidak dapat mengatur
MAP
secara dinamis saat runtime. Jika Anda tidak perlu melakukan itu, maka cukup deklarasikanMAP
sebagai astatic final HashMap<>
dan Anda dijamin publikasi aman.Dalam praktiknya, aturannya sederhana untuk akses aman ke "objek yang tidak pernah dimodifikasi":
Jika Anda menerbitkan objek yang tidak dapat diubah secara inheren (seperti yang dinyatakan dalam semua bidang
final
) dan:final
bidang (termasukstatic final
untuk anggota statis).Itu dia!
Dalam praktiknya, ini sangat efisien. Penggunaan
static final
bidang, misalnya, memungkinkan JVM untuk mengasumsikan nilainya tidak berubah selama umur program dan mengoptimalkannya dengan berat. Penggunaanfinal
bidang anggota memungkinkan sebagian besar arsitektur membaca bidang dengan cara yang setara dengan bidang biasa, membaca dan tidak menghambat optimalisasi lebih lanjut c .Akhirnya, penggunaan
volatile
memang memiliki beberapa dampak: tidak ada penghalang perangkat keras yang diperlukan pada banyak arsitektur (seperti x86, khususnya yang tidak memungkinkan membaca lulus membaca), tetapi beberapa optimasi dan penataan ulang mungkin tidak terjadi pada waktu kompilasi - tetapi ini efeknya umumnya kecil. Sebagai gantinya, Anda benar-benar mendapatkan lebih dari yang Anda minta - Anda tidak hanya dapat mempublikasikannya dengan amanHashMap
, Anda dapat menyimpan lebih banyak lagi yang tidak dimodifikasiHashMap
seperti yang Anda inginkan dengan referensi yang sama dan yakinlah bahwa semua pembaca akan melihat peta yang diterbitkan dengan aman .Untuk detail lebih banyak darah, lihat Shipilev atau FAQ ini oleh Manson dan Goetz .
[1] Secara langsung mengutip dari shipilev .
sebuah suara yang rumit, tapi apa yang saya maksud adalah bahwa Anda dapat menetapkan referensi pada saat konstruksi - baik pada titik deklarasi atau dalam konstruktor (bidang anggota) atau initializer statis (bidang statis).
b Secara opsional, Anda dapat menggunakan
synchronized
metode untuk mendapatkan / mengatur,AtomicReference
atau sesuatu, tetapi kita sedang berbicara tentang pekerjaan minimum yang dapat Anda lakukan.c Beberapa arsitektur dengan model memori yang sangat lemah (saya sedang melihat Anda , Alpha) mungkin memerlukan beberapa jenis pembatas baca sebelum
final
dibaca - tetapi ini sangat jarang saat ini.sumber
never modify
HashMap
bukan berartistate of the map object
thread aman saya pikir. Tuhan tahu implementasi perpustakaan, jika dokumen resmi tidak mengatakan itu aman.get()
sebenarnya dapat melakukan beberapa penulisan, misalnya memperbarui beberapa statistik (atau dalam hal akses-dipesanLinkedHashMap
memperbarui urutan akses). Jadi kelas yang ditulis dengan baik harus menyediakan beberapa dokumentasi yang membuatnya jelas jika ...const
benar-benar hanya-baca dalam arti seperti itu (secara internal, mereka mungkin masih melakukan penulisan, tetapi ini harus dibuat aman-utas). Tidak adaconst
kata kunci di Jawa dan saya tidak mengetahui adanya jaminan selimut yang didokumentasikan, tetapi secara umum kelas perpustakaan standar berperilaku seperti yang diharapkan, dan pengecualian didokumentasikan (lihatLinkedHashMap
contoh di mana RO ops sepertiget
secara eksplisit disebutkan sebagai tidak aman).HashMap
kami benar-benar memiliki hak dalam dokumentasi perilaku utas keselamatan untuk kelas ini: Jika beberapa utas mengakses peta hash secara bersamaan, dan setidaknya satu utas mengubah peta secara struktural, itu harus disinkronkan secara eksternal. (Modifikasi struktural adalah operasi apa pun yang menambah atau menghapus satu atau lebih pemetaan; hanya mengubah nilai yang terkait dengan kunci yang sudah ada dalam instance bukan modifikasi struktural.)HashMap
metode yang kami harapkan hanya-baca, hanya-baca, karena mereka tidak secara struktural memodifikasiHashMap
. Tentu saja, jaminan ini mungkin tidak berlaku untukMap
implementasi lain yang sewenang-wenang , tetapi pertanyaannya adalah tentangHashMap
secara spesifik.Jeremy Manson, dewa ketika datang ke Java Memory Model, memiliki tiga bagian blog tentang topik ini - karena pada dasarnya Anda menanyakan pertanyaan "Apakah aman untuk mengakses HashMap yang tidak dapat diubah" - jawabannya adalah ya. Tetapi Anda harus menjawab predikat untuk pertanyaan itu yaitu - "Apakah HashMap saya tidak berubah". Jawabannya mungkin mengejutkan Anda - Java memiliki seperangkat aturan yang relatif rumit untuk menentukan imutabilitas.
Untuk info lebih lanjut tentang topik ini, baca posting blog Jeremy:
Bagian 1 tentang Ketidakmampuan di Jawa: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html
Bagian 2 tentang Ketidakmampuan di Jawa: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html
Bagian 3 tentang Ketidakmampuan di Jawa: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html
sumber
Pembacaan aman dari sudut pandang sinkronisasi tetapi tidak dari sudut pandang memori. Ini adalah sesuatu yang banyak disalahpahami di antara pengembang Java termasuk di sini di Stackoverflow. (Perhatikan peringkat jawaban ini sebagai bukti.)
Jika Anda memiliki utas lain yang berjalan, mereka mungkin tidak melihat salinan terbaru dari HashMap jika tidak ada memori yang dihapus dari utas saat ini. Memori menulis terjadi melalui penggunaan kata kunci yang disinkronkan atau volatil, atau melalui penggunaan beberapa konstruksi konkurensi java.
Lihat artikel Brian Goetz pada Java Memory Model baru untuk detailnya.
sumber
Setelah sedikit lebih mencari, saya menemukan ini di java doc (penekanan milik saya):
Ini tampaknya menyiratkan bahwa itu akan aman, dengan asumsi kebalikan dari pernyataan itu benar.
sumber
Satu catatan adalah bahwa dalam beberapa keadaan, get () dari HashMap yang tidak disinkronkan dapat menyebabkan loop tak terbatas. Ini dapat terjadi jika put bersamaan () menyebabkan pengulangan peta.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
sumber
Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
Ada twist penting sekalipun. Aman untuk mengakses peta, tetapi secara umum tidak dijamin bahwa semua utas akan melihat kondisi yang sama persis (dan dengan demikian nilainya) dari HashMap. Ini mungkin terjadi pada sistem multiprosesor di mana modifikasi pada HashMap dilakukan oleh satu utas (misalnya, yang mengisi itu) dapat duduk dalam cache CPU dan tidak akan terlihat oleh utas yang berjalan pada CPU lain, sampai operasi pagar memori dilakukan memastikan koherensi cache. Spesifikasi Bahasa Jawa eksplisit untuk yang satu ini: solusinya adalah memperoleh kunci (disinkronkan (...)) yang memancarkan operasi pagar memori. Jadi, jika Anda yakin bahwa setelah mengisi HashMap, masing-masing utas memperoleh kunci APA PUN, maka sejak saat itu untuk mengakses HashMap dari utas apa pun hingga HashMap dimodifikasi lagi.
sumber
Menurut http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Keamanan inisialisasi Anda dapat menjadikan HashMap Anda bidang terakhir dan setelah konstruktor selesai, ia akan diterbitkan dengan aman.
... Di bawah model memori baru, ada sesuatu yang mirip dengan hubungan sebelum terjadi antara penulisan bidang terakhir di konstruktor dan beban awal referensi bersama ke objek di thread lain. ...
sumber
Jadi skenario yang Anda gambarkan adalah bahwa Anda perlu memasukkan banyak data ke dalam Peta, maka ketika Anda selesai mengisinya, Anda memperlakukannya sebagai tidak berubah. Salah satu pendekatan yang "aman" (yang berarti Anda menegakkan bahwa itu benar-benar dianggap abadi) adalah mengganti referensi dengan
Collections.unmodifiableMap(originalMap)
saat Anda siap untuk membuatnya tidak berubah.Untuk contoh seberapa buruk peta bisa gagal jika digunakan secara bersamaan, dan solusi yang disarankan yang saya sebutkan, lihat entri parade bug ini: bug_id = 6423457
sumber
Pertanyaan ini dibahas dalam buku "Java Concurrency in Practice" karya Brian Goetz (Listing 16.8, halaman 350):
Karena
states
dinyatakan sebagaifinal
dan inisialisasi dilakukan dalam konstruktor kelas pemilik, utas apa pun yang kemudian membaca peta ini dijamin akan melihatnya pada saat konstruktor selesai, asalkan tidak ada utas lain yang akan mencoba mengubah isi peta.sumber
Berhati-hatilah bahwa bahkan dalam kode single-threaded, mengganti ConcurrentHashMap dengan HashMap mungkin tidak aman. ConcurrentHashMap melarang null sebagai kunci atau nilai. HashMap tidak melarang mereka (jangan tanya).
Jadi dalam situasi yang tidak mungkin kode Anda yang ada dapat menambahkan nol ke koleksi selama pengaturan (mungkin dalam beberapa kasus kegagalan), mengganti koleksi seperti yang dijelaskan akan mengubah perilaku fungsional.
Yang mengatakan, asalkan Anda tidak melakukan hal lain bersamaan membaca dari HashMap aman.
[Sunting: oleh "concurrent reads", maksud saya tidak ada modifikasi bersamaan.
Jawaban lain menjelaskan bagaimana memastikan ini. Salah satu caranya adalah membuat peta tidak berubah, tetapi itu tidak perlu. Sebagai contoh, model memori JSR133 secara eksplisit mendefinisikan mulai thread untuk menjadi tindakan yang disinkronkan, yang berarti bahwa perubahan yang dibuat pada thread A sebelum memulai thread B terlihat di thread B.
Maksud saya bukan untuk menentang jawaban yang lebih rinci tentang Model Memori Java. Jawaban ini dimaksudkan untuk menunjukkan bahwa selain dari masalah konkurensi, ada setidaknya satu perbedaan API antara ConcurrentHashMap dan HashMap, yang dapat mengacaukan bahkan sebuah program berulir tunggal yang menggantikan satu dengan yang lainnya.]
sumber
http://www.docjar.com/html/api/java/util/HashMap.java.html
di sini adalah sumber untuk HashMap. Seperti yang Anda tahu, sama sekali tidak ada kode penguncian / mutex di sana.
Ini berarti bahwa walaupun boleh untuk membaca dari HashMap dalam situasi multithreaded, saya pasti akan menggunakan ConcurrentHashMap jika ada beberapa penulisan.
Yang menarik adalah bahwa .NET HashTable dan Kamus <K, V> telah membangun kode sinkronisasi.
sumber
Jika inisialisasi dan setiap put disinkronkan, Anda menyimpan.
Kode berikut disimpan karena classloader akan menangani sinkronisasi:
Kode berikut disimpan karena penulisan volatile akan menjaga sinkronisasi.
Ini juga akan berfungsi jika variabel anggota final karena final juga volatile. Dan jika metodenya adalah konstruktor.
sumber