Penting untuk dipahami bahwa ada dua aspek untuk keselamatan ulir.
- kontrol pelaksanaan, dan
- visibilitas memori
Yang pertama berkaitan dengan pengontrolan ketika kode dijalankan (termasuk urutan instruksi dijalankan) dan apakah dapat dieksekusi secara bersamaan, dan yang kedua berkaitan dengan ketika efek dalam memori dari apa yang telah dilakukan dapat dilihat oleh utas lainnya. Karena setiap CPU memiliki beberapa tingkat cache antara itu dan memori utama, utas yang berjalan pada CPU atau inti yang berbeda dapat melihat "memori" secara berbeda pada setiap waktu tertentu karena utas diizinkan untuk mendapatkan dan bekerja pada salinan pribadi dari memori utama.
Penggunaan synchronized
mencegah utas lainnya mendapatkan monitor (atau kunci) untuk objek yang sama , sehingga mencegah semua blok kode yang dilindungi oleh sinkronisasi pada objek yang sama dari mengeksekusi secara bersamaan. Sinkronisasi juga menciptakan penghalang memori "terjadi-sebelum", menyebabkan batasan visibilitas memori sehingga apa pun yang dilakukan sampai titik beberapa thread melepaskan kunci muncul ke thread lain kemudian mendapatkan kunci yang sama dengan yang terjadi sebelum memperoleh kunci. Dalam istilah praktis, pada perangkat keras saat ini, ini biasanya menyebabkan pembilasan cache CPU ketika monitor diperoleh dan menulis ke memori utama ketika dirilis, keduanya (relatif) mahal.
Menggunakan volatile
, di sisi lain, pasukan semua akses (membaca atau menulis) dengan variabel yang mudah menguap terjadi ke memori utama, efektif menjaga variabel yang mudah menguap dari cache CPU. Ini dapat berguna untuk beberapa tindakan di mana hanya diperlukan agar visibilitas variabel menjadi benar dan urutan akses tidak penting. Menggunakan volatile
juga mengubah perawatan long
dan double
membutuhkan akses ke mereka untuk menjadi atom; pada beberapa perangkat keras (lama) ini mungkin memerlukan kunci, meskipun tidak pada perangkat keras 64 bit modern. Di bawah model memori baru (JSR-133) untuk Java 5+, semantik volatile telah diperkuat menjadi hampir sekuat yang disinkronkan sehubungan dengan visibilitas memori dan pemesanan instruksi (lihat http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Untuk tujuan visibilitas, setiap akses ke bidang yang mudah menguap bertindak seperti setengah sinkronisasi.
Di bawah model memori baru, masih benar bahwa variabel volatil tidak dapat disusun ulang satu sama lain. Perbedaannya adalah bahwa sekarang tidak lagi begitu mudah untuk menyusun ulang akses bidang normal di sekitar mereka. Menulis ke bidang yang mudah menguap memiliki efek memori yang sama dengan rilis monitor, dan membaca dari bidang yang mudah menguap memiliki efek memori yang sama dengan yang diperoleh monitor. Akibatnya, karena model memori baru menempatkan batasan yang lebih ketat pada pengurutan ulang akses bidang volatil dengan akses bidang lainnya, volatil atau tidak, apa pun yang terlihat berulir A
ketika menulis ke bidang volatil f
menjadi terlihat berulir B
ketika dibaca f
.
- JSR 133 (Model Memori Java) FAQ
Jadi, sekarang kedua bentuk penghalang memori (di bawah JMM saat ini) menyebabkan penghalang pemesanan ulang instruksi yang mencegah kompiler atau run-time dari memesan ulang instruksi di penghalang. Di JMM lama, volatile tidak mencegah pemesanan ulang. Ini bisa menjadi penting, karena selain dari hambatan ingatan, satu-satunya batasan yang diberlakukan adalah bahwa, untuk utas tertentu , efek bersih dari kode adalah sama seperti jadinya jika instruksi dieksekusi tepat sesuai urutan di mana mereka muncul di sumber.
Salah satu penggunaan volatile adalah untuk objek yang dibagikan tetapi tidak dapat diubah diciptakan kembali dengan cepat, dengan banyak utas lainnya mengambil referensi ke objek tersebut pada titik tertentu dalam siklus eksekusi mereka. Satu membutuhkan utas lainnya untuk mulai menggunakan objek yang dibuat kembali setelah dipublikasikan, tetapi tidak memerlukan overhead tambahan sinkronisasi penuh dan pertikaian yang menyertainya dan pembilasan cache.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Berbicara kepada pertanyaan baca-perbarui-tulis Anda, secara khusus. Pertimbangkan kode tidak aman berikut:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Sekarang, dengan metode updateCounter () tidak disinkronkan, dua utas dapat memasukkannya secara bersamaan. Di antara banyak permutasi dari apa yang bisa terjadi, satu adalah bahwa thread-1 melakukan tes untuk penghitung == 1000 dan menemukan itu benar dan kemudian ditangguhkan. Kemudian thread-2 melakukan tes yang sama dan juga melihatnya benar dan ditangguhkan. Kemudian utas-1 melanjutkan dan menetapkan penghitung ke 0. Kemudian utas-2 melanjutkan dan kembali menetapkan pencacah ke 0 karena melewatkan pembaruan dari utas-1. Ini juga dapat terjadi bahkan jika penggantian ulir tidak terjadi seperti yang saya jelaskan, tetapi hanya karena dua salinan cache yang berbeda hadir dalam dua inti CPU yang berbeda dan masing-masing ulir berjalan pada inti yang terpisah. Dalam hal ini, satu utas dapat memiliki penghitung pada satu nilai dan yang lain dapat memiliki penghitung pada nilai yang sama sekali berbeda hanya karena caching.
Apa yang penting dalam contoh ini adalah bahwa penghitung variabel dibaca dari memori utama ke dalam cache, diperbarui dalam cache dan hanya ditulis kembali ke memori utama di beberapa titik tak tentu kemudian ketika penghalang memori terjadi atau ketika memori cache diperlukan untuk hal lain. Membuat penghitung volatile
tidak mencukupi untuk keamanan thread kode ini, karena pengujian untuk maksimum dan penugasan adalah operasi terpisah, termasuk kenaikan yang merupakan serangkaian read+increment+write
instruksi mesin non-atom , seperti:
MOV EAX,counter
INC EAX
MOV counter,EAX
Variabel volatil hanya berguna ketika semua operasi yang dilakukan adalah "atom", seperti contoh saya di mana referensi ke objek yang sepenuhnya terbentuk hanya dibaca atau ditulis (dan, tentu saja, biasanya hanya ditulis dari satu titik). Contoh lain adalah referensi array yang mudah menguap yang mendukung daftar copy-on-write, asalkan array hanya dibaca dengan terlebih dahulu mengambil salinan referensi lokal ke sana.