Perbedaan antara volatile dan disinkronkan di Jawa


233

Saya bertanya-tanya perbedaan antara mendeklarasikan variabel sebagai volatiledan selalu mengakses variabel dalam sebuah synchronized(this)blok di Jawa?

Menurut artikel ini http://www.javamex.com/tutorials/synchronization_volatile.shtml ada banyak yang bisa dikatakan dan ada banyak perbedaan tetapi juga beberapa kesamaan.

Saya sangat tertarik dengan informasi ini:

...

  • akses ke variabel volatil tidak pernah berpotensi untuk diblokir: kita hanya melakukan pembacaan atau penulisan sederhana, jadi tidak seperti blok yang disinkronkan, kita tidak akan pernah memegang kunci apa pun;
  • karena mengakses variabel volatil tidak pernah memegang kunci, itu tidak cocok untuk kasus-kasus di mana kita ingin membaca-memperbarui-menulis sebagai operasi atom (kecuali kita siap untuk "melewatkan pembaruan");

Apa yang dimaksud dengan baca-perbarui-tulis ? Bukankah menulis juga pembaruan atau mereka hanya berarti bahwa pembaruan itu adalah menulis yang tergantung pada baca?

Yang paling penting, kapan lebih tepat untuk mendeklarasikan variabel volatiledaripada mengaksesnya melalui synchronizedblok? Apakah ini ide yang baik untuk digunakan volatileuntuk variabel yang bergantung pada input? Misalnya, ada variabel yang dipanggil renderyang dibaca melalui perenderan loop dan diatur oleh peristiwa penekanan tombol?

Jawaban:


383

Penting untuk dipahami bahwa ada dua aspek untuk keselamatan ulir.

  1. kontrol pelaksanaan, dan
  2. 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 synchronizedmencegah 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 volatilejuga mengubah perawatan longdan doublemembutuhkan 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 Aketika menulis ke bidang volatil fmenjadi terlihat berulir Bketika 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 volatiletidak mencukupi untuk keamanan thread kode ini, karena pengujian untuk maksimum dan penugasan adalah operasi terpisah, termasuk kenaikan yang merupakan serangkaian read+increment+writeinstruksi 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.


5
Terima kasih banyak! Contoh dengan penghitungnya mudah dimengerti. Namun, ketika semuanya menjadi nyata, itu sedikit berbeda.
Albus Dumbledore

"Secara praktis, pada perangkat keras saat ini, ini biasanya menyebabkan pembilasan cache CPU ketika monitor diperoleh dan menulis ke memori utama ketika dilepaskan, yang keduanya mahal (relatif berbicara)." . Ketika Anda mengatakan cache CPU, apakah itu sama dengan Java Stacks lokal untuk setiap utas? atau apakah utas memiliki Heap versi lokalnya sendiri? Mohon maaf jika saya konyol di sini.
NishM

1
@nishm Ini tidak sama, tetapi itu akan mencakup cache lokal dari utas yang terlibat. .
Lawrence Dol

1
@ MarianPaździoch: Peningkatan atau penurunan BUKAN baca atau tulis, itu baca dan tulis; itu adalah membaca register, lalu kenaikan register, lalu menulis kembali ke memori. Membaca dan menulis secara individual bersifat atomik, tetapi beberapa operasi semacam itu tidak.
Lawrence Dol

2
Jadi, menurut FAQ, tidak hanya tindakan yang dilakukan karena akuisisi kunci dibuat terlihat setelah membuka kunci, tetapi semua tindakan yang dilakukan oleh utas itu dibuat terlihat. Bahkan tindakan yang dilakukan sebelum kunci akuisisi.
Lii

97

volatile adalah pengubah bidang , sementara yang disinkronkan memodifikasi blok dan metode kode . Jadi kita dapat menentukan tiga variasi pengakses sederhana menggunakan dua kata kunci tersebut:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()mengakses nilai yang saat ini disimpan di i1utas saat ini. Thread dapat memiliki salinan variabel lokal, dan data tidak harus sama dengan data yang disimpan di utas lainnya. Secara khusus, utas lain mungkin telah diperbarui i1di utasnya, tetapi nilai di utas saat ini bisa berbeda dari yang ada. nilai yang diperbarui. Sebenarnya Java memiliki gagasan tentang memori "utama", dan ini adalah memori yang menyimpan nilai "benar" saat ini untuk variabel. Thread dapat memiliki salinan data sendiri untuk variabel, dan salinan utas dapat berbeda dari memori "utama". Jadi sebenarnya, memori "utama" mungkin memiliki nilai 1 untuk i1, untuk thread1 memiliki nilai 2 untuk i1dan untuk thread2memiliki nilai 3 karena i1jika thread1 dan thread2 keduanya memperbarui i1 tetapi nilai yang diperbarui tersebut belum disebarkan ke memori "utama" atau utas lainnya.

Di sisi lain, geti2()secara efektif mengakses nilai dari i2memori "utama". Variabel volatil tidak diperbolehkan memiliki salinan lokal dari variabel yang berbeda dari nilai yang saat ini disimpan dalam memori "utama". Secara efektif, variabel yang dinyatakan tidak stabil harus memiliki data yang disinkronkan di semua utas, sehingga setiap kali Anda mengakses atau memperbarui variabel di utas apa pun, semua utas lainnya segera melihat nilai yang sama. Umumnya variabel volatile memiliki akses dan pembaruan overhead yang lebih tinggi daripada variabel "biasa". Umumnya utas diizinkan memiliki salinan data sendiri untuk efisiensi yang lebih baik.

Ada dua perbedaan antara volitile dan disinkronkan.

Pertama yang disinkronkan memperoleh dan melepaskan kunci pada monitor yang hanya dapat memaksa satu utas pada satu waktu untuk menjalankan blok kode. Itulah aspek yang cukup terkenal untuk disinkronkan. Tetapi disinkronkan juga menyinkronkan memori. Faktanya, sinkronisasi menyinkronkan seluruh memori utas dengan memori "utama". Jadi mengeksekusi geti3()melakukan hal berikut:

  1. Utas mendapatkan kunci pada monitor untuk objek ini.
  2. Memori utas menyiram semua variabelnya, yaitu ia memiliki semua variabelnya secara efektif dibaca dari memori "utama".
  3. Blok kode dieksekusi (dalam hal ini mengatur nilai kembali ke nilai i3 saat ini, yang mungkin baru saja direset dari memori "utama").
  4. (Setiap perubahan pada variabel biasanya sekarang akan ditulis ke memori "utama", tetapi untuk geti3 () kami tidak memiliki perubahan.)
  5. Utas melepaskan kunci pada monitor untuk objek ini.

Jadi di mana volatile hanya menyinkronkan nilai satu variabel antara memori ulir dan memori "utama", disinkronkan menyinkronkan nilai semua variabel antara memori ulir dan memori "utama", dan mengunci dan melepaskan monitor untuk boot. Jelas disinkronkan cenderung memiliki lebih banyak overhead daripada volatile.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html


35
-1, Volatile tidak memperoleh kunci, ia menggunakan arsitektur CPU yang mendasarinya untuk memastikan visibilitas di semua utas setelah penulisan.
Michael Barker

Perlu dicatat bahwa mungkin ada beberapa kasus di mana kunci dapat digunakan untuk menjamin keaslian penulisan. Misalnya menulis panjang pada platform 32 bit yang tidak mendukung hak lebar yang diperluas. Intel menghindari ini dengan menggunakan register SSE2 (lebar 128 bit) untuk menangani long volatile. Namun, mempertimbangkan volatile sebagai kunci kemungkinan akan menyebabkan bug jahat dalam kode Anda.
Michael Barker

2
Semantik penting yang dibagikan oleh mengunci variabel volatil adalah keduanya menyediakan Happens-Before edge (Java 1.5 dan yang lebih baru). Memasuki blok yang disinkronkan, mengeluarkan kunci dan membaca dari volatile semuanya dianggap sebagai "memperoleh" dan melepaskan kunci, keluar dari blok yang disinkronkan dan menulis volatile adalah semua bentuk "rilis".
Michael Barker

20

synchronizedadalah pengubah batasan akses tingkat metode / blok. Ini akan memastikan bahwa satu utas memiliki kunci untuk bagian kritis. Hanya utas, yang memiliki kunci yang dapat memasuki synchronizedblok. Jika utas lain mencoba mengakses bagian kritis ini, mereka harus menunggu sampai pemilik saat ini melepaskan kunci.

volatileadalah pengubah akses variabel yang memaksa semua utas untuk mendapatkan nilai terbaru dari variabel dari memori utama. Tidak diperlukan penguncian untuk mengakses volatilevariabel. Semua utas dapat mengakses nilai variabel volatil pada saat yang sama.

Contoh yang baik untuk menggunakan variabel volatile: Datevariabel.

Asumsikan bahwa Anda telah membuat variabel Tanggal volatile. Semua utas, yang mengakses variabel ini selalu mendapatkan data terbaru dari memori utama sehingga semua utas menunjukkan nilai Tanggal nyata (aktual). Anda tidak perlu utas berbeda yang menunjukkan waktu berbeda untuk variabel yang sama. Semua utas harus menunjukkan nilai Tanggal yang benar.

masukkan deskripsi gambar di sini

Lihat artikel ini untuk pemahaman volatilekonsep yang lebih baik .

Lawrence Dol cleary menjelaskan read-write-update query.

Mengenai pertanyaan Anda yang lain

Kapan lebih tepat untuk mendeklarasikan variabel yang tidak stabil daripada mengaksesnya melalui sinkronisasi?

Anda harus menggunakan volatilejika Anda berpikir semua utas harus mendapatkan nilai aktual dari variabel secara real time seperti contoh yang saya jelaskan untuk variabel Tanggal.

Apakah ide yang baik untuk menggunakan volatile untuk variabel yang bergantung pada input?

Jawabannya akan sama dengan di kueri pertama.

Lihat artikel ini untuk pemahaman yang lebih baik.


Jadi membaca dapat terjadi pada waktu yang bersamaan, dan semua utas akan membaca nilai terbaru karena CPU tidak men-cache memori utama ke cache thread CPU, tetapi bagaimana dengan menulis? Menulis tidak harus bersamaan dengan benar? Pertanyaan kedua: jika suatu blok disinkronkan, tetapi variabelnya tidak mudah menguap, nilai suatu variabel dalam blok yang disinkronkan masih dapat diubah oleh utas lain dalam blok kode lain, kan?
the_prole

11

tl; dr :

Ada 3 masalah utama dengan multithreading:

1) Kondisi Balapan

2) Memori cache / basi

3) Pengoptimal dan pengoptimalan CPU

volatiledapat menyelesaikan 2 & 3, tetapi tidak dapat menyelesaikan 1. synchronized/ kunci eksplisit dapat menyelesaikan 1, 2 & 3.

Elaborasi :

1) Pertimbangkan utas ini kode tidak aman:

x++;

Walaupun terlihat seperti satu operasi, sebenarnya 3: membaca nilai x saat ini dari memori, menambahkan 1 ke dalamnya, dan menyimpannya kembali ke memori. Jika beberapa utas mencoba melakukannya secara bersamaan, hasil operasi tidak ditentukan. Jika xawalnya adalah 1, setelah 2 utas yang mengoperasikan kode itu mungkin 2 dan mungkin 3, tergantung pada utas yang menyelesaikan bagian mana dari operasi sebelum kontrol dipindahkan ke utas lainnya. Ini adalah bentuk kondisi balapan .

Menggunakan synchronizedpada blok kode membuatnya atomik - artinya membuatnya seolah-olah 3 operasi terjadi sekaligus, dan tidak ada cara untuk utas lain untuk datang di tengah dan mengganggu. Jadi jika xdulu 1, dan 2 utas mencoba untuk membentuk sebelumnya x++kita tahu pada akhirnya itu akan sama dengan 3. Jadi itu memecahkan masalah kondisi balapan.

synchronized (this) {
   x++; // no problem now
}

Menandai xsebagai volatiletidak membuat x++;atom, jadi itu tidak menyelesaikan masalah ini.

2) Selain itu, utas memiliki konteksnya sendiri - yaitu mereka dapat menyimpan nilai dari memori utama. Itu berarti bahwa beberapa utas dapat memiliki salinan variabel, tetapi mereka beroperasi pada copy pekerjaan mereka tanpa berbagi keadaan variabel baru di antara utas lainnya.

Pertimbangkan itu di satu utas x = 10;,. Dan agak kemudian, di utas lainnya x = 20;,. Perubahan nilai xmungkin tidak muncul di utas pertama, karena utas lain telah menyimpan nilai baru ke memori yang berfungsi, tetapi belum menyalinnya ke memori utama. Atau itu memang menyalinnya ke memori utama, tetapi utas pertama belum memperbarui salinan yang berfungsi. Jadi jika sekarang utas pertama memeriksa if (x == 20)jawabannya akan false.

Menandai variabel sebagai volatiledasarnya memberitahu semua utas untuk melakukan operasi baca dan tulis pada memori utama saja. synchronizedmemberitahu setiap utas untuk memperbarui nilai mereka dari memori utama ketika mereka memasuki blok, dan membuang hasilnya kembali ke memori utama ketika mereka keluar dari blok.

Perhatikan bahwa tidak seperti perlombaan data, memori basi tidak mudah (kembali) diproduksi, karena bagaimanapun memori flush ke memori utama terjadi.

3) Pengompilasi dan CPU dapat (tanpa segala bentuk sinkronisasi antara utas) memperlakukan semua kode sebagai utas tunggal. Artinya dapat melihat beberapa kode, yang sangat berarti dalam aspek multithreading, dan memperlakukannya seolah-olah itu adalah utas tunggal, di mana itu tidak begitu berarti. Jadi dapat melihat kode dan memutuskan, demi optimasi, untuk menyusun ulang, atau bahkan menghapus bagian dari itu sepenuhnya, jika tidak tahu bahwa kode ini dirancang untuk bekerja pada banyak utas.

Pertimbangkan kode berikut:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

Anda akan berpikir bahwa threadB hanya dapat mencetak 20 (atau tidak mencetak apa-apa sama sekali jika threadB if-check dieksekusi sebelum pengaturan bke true), seperti byang disetel ke true hanya setelah xdiatur ke 20, tetapi kompiler / CPU mungkin memutuskan untuk memesan ulang threadA, dalam hal ini threadB juga dapat mencetak 10. Menandai bsebagai volatilememastikan bahwa itu tidak akan disusun ulang (atau dibuang dalam kasus-kasus tertentu). Yang berarti threadB hanya bisa mencetak 20 (atau tidak sama sekali). Menandai metode yang disinkronkan akan mencapai hasil yang sama. Juga menandai variabel sebagai volatilehanya memastikan bahwa itu tidak akan disusun ulang, tetapi segala sesuatu sebelum / setelah itu masih dapat disusun ulang, sehingga sinkronisasi dapat lebih cocok dalam beberapa skenario.

Perhatikan bahwa sebelum Java 5 New Memory Model, volatile tidak menyelesaikan masalah ini.


1
"Walaupun terlihat seperti satu operasi, sebenarnya 3: membaca nilai x saat ini dari memori, menambahkan 1 ke dalamnya, dan menyimpannya kembali ke memori." - Benar, karena nilai dari memori harus melalui sirkuit CPU agar dapat ditambahkan / dimodifikasi. Meskipun ini hanya berubah menjadi INCoperasi Majelis tunggal , operasi CPU yang mendasarinya masih 3 kali lipat dan membutuhkan penguncian untuk keamanan thread. Poin bagus. Meskipun, INC/DECperintah dapat ditandai secara atom dalam perakitan dan masih menjadi 1 operasi atom.
Zombies

@ Zombies jadi ketika saya membuat blok yang disinkronkan untuk x ++, apakah ini mengubahnya menjadi INC / DEC atom yang ditandai atau apakah menggunakan kunci biasa?
David Refaeli

Saya tidak tahu! Apa yang saya tahu adalah bahwa INC / DEC bukan atom karena untuk CPU, ia harus memuat nilai dan BACA dan juga MENULISkannya (ke memori), sama seperti operasi aritmatika lainnya.
Zombies
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.