Seri Pemrograman Bersamaan Java [belum selesai]:
• Pemrograman konkurensi Java: Teori Inti
• Pemrograman Bersamaan Java: Sinkronisasi dan Prinsip Implementasinya
• Pemrograman Bersamaan Java: Optimalisasi yang mendasari yang disinkronkan (kunci ringan, kunci bias)
• Pemrograman Bersamaan Java: Kolaborasi antara utas (tunggu/beri tahu/tidur/hasil/gabung)
• Pemrograman Bersamaan Java: Penggunaan Volatile dan Prinsip -prinsipnya
1. Peran volatile
Dalam artikel "Pemrograman Konkurensi Java: Teori Inti", kami telah menyebutkan masalah visibilitas, ketertiban, dan atomisitas. Biasanya, kita dapat menyelesaikan masalah ini melalui kata kunci yang disinkronkan. Namun, jika Anda memiliki pemahaman tentang prinsip sinkronisasi, Anda harus tahu bahwa disinkronkan adalah operasi kelas yang relatif berat dan memiliki dampak yang relatif besar pada kinerja sistem. Oleh karena itu, jika ada solusi lain, kami biasanya menghindari penggunaan yang disinkronkan untuk menyelesaikan masalah. Kata kunci yang mudah menguap adalah solusi lain yang disediakan di Java untuk menyelesaikan masalah visibilitas dan ketertiban. Mengenai atomisitas, ini juga merupakan titik bahwa setiap orang cenderung salah paham: satu operasi baca/tulis variabel volatil dapat memastikan atomisitas, seperti variabel tipe panjang dan ganda, tetapi tidak dapat menjamin atomisitas operasi I ++, karena pada dasarnya, i ++ adalah operasi yang dibaca dan menulis dua kali.
2. Penggunaan volatile
Mengenai penggunaan volatile, kita dapat menggunakan beberapa contoh untuk menggambarkan penggunaan dan skenario.
1. Mencegah pemesanan ulang
Mari kita analisis masalah pemesanan ulang dari salah satu contoh paling klasik. Setiap orang harus terbiasa dengan implementasi model Singleton, dan dalam lingkungan yang bersamaan, kita biasanya dapat menggunakan metode Penguncian Double Check (DCL) untuk mengimplementasikannya. Kode sumber adalah sebagai berikut:
paket com.paddx.test.concurrent; Singleton kelas publik {public static volatile singleton singleton; / *** Konstruktor adalah pribadi, melarang instantiasi eksternal*/ singleton pribadi () {}; public static singleton getInstance () {if (singleton == null) {disinkronkan (singleton) {if (singleton == null) {singleton = singleton baru (); }} return singleton; }}Sekarang mari kita analisis mengapa kita perlu menambahkan kata kunci yang mudah menguap antara variabel singleton. Untuk memahami masalah ini, Anda harus terlebih dahulu memahami proses konstruksi objek. Instantiating suatu objek sebenarnya dapat dibagi menjadi tiga langkah:
(1) Alokasikan ruang memori.
(2) menginisialisasi objek.
(3) Tetapkan alamat ruang memori ke referensi yang sesuai.
Namun, karena sistem operasi dapat memesan ulang instruksi, proses di atas juga dapat menjadi proses berikut:
(1) Alokasikan ruang memori.
(2) Tetapkan alamat ruang memori ke referensi yang sesuai.
(3) menginisialisasi objek
Jika proses ini adalah prosesnya, referensi objek yang tidak diinisialisasi dapat diekspos dalam lingkungan multi-utas, menghasilkan hasil yang tidak dapat diprediksi. Oleh karena itu, untuk mencegah pemesanan ulang proses ini, kita perlu mengatur variabel ke variabel tipe volatile.
2. Mencapai visibilitas
Masalah visibilitas terutama mengacu pada satu utas yang memodifikasi nilai variabel bersama, sedangkan utas lainnya tidak dapat melihatnya. Alasan utama untuk masalah visibilitas adalah bahwa setiap utas memiliki area cache sendiri - memori kerja utas. Kata kunci yang mudah menguap dapat secara efektif menyelesaikan masalah ini. Mari kita lihat contoh -contoh berikut untuk mengetahui fungsinya:
paket com.paddx.test.concurrent; kelas publik volatiletest {int a = 1; int b = 2; public void change () {a = 3; b = a; } public void print () {System.out.println ("b ="+b+"; a ="+a); } public static void main (string [] args) {while (true) {final volatiletest test = new volatiletest (); utas baru (runnable baru () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printstacktrace ();} test.change ();}}). start (); utas baru (runnable baru () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printstacktrace ();} test.print ();}}). start (); }}}Secara intuitif, hanya ada dua hasil yang mungkin untuk kode ini: b = 3; a = 3 atau b = 2; a = 1. Namun, menjalankan kode di atas (mungkin butuh sedikit lebih lama), Anda akan menemukan bahwa selain dua hasil sebelumnya, ada juga hasil ketiga:
...... b = 2; a = 1b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 1b = 3; a = 3b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 3 ...
Mengapa hasil seperti b = 3; a = 1 muncul? Dalam keadaan normal, jika Anda menjalankan metode perubahan terlebih dahulu dan kemudian menjalankan metode cetak, hasil output harus b = 3; a = 3. Sebaliknya, jika Anda menjalankan metode cetak terlebih dahulu dan kemudian menjalankan metode perubahan, hasilnya harus b = 2; a = 1. Jadi bagaimana hasil b = 3; a = 1 keluar? Alasannya adalah bahwa utas pertama memodifikasi nilai A = 3, tetapi tidak terlihat oleh utas kedua, jadi hasil ini terjadi. Jika kedua A dan B diubah menjadi variabel tipe volatil dan dieksekusi, hasil b = 3; a = 1 tidak akan pernah muncul lagi.
3. Pastikan atomisitas
Masalah atomisitas telah dijelaskan di atas. Volatile hanya dapat menjamin atomisitas untuk membaca/menulis tunggal. Masalah ini dapat dijelaskan dalam JLS:
17.7 Perlakuan non-atomik ganda dan panjang untuk keperluan model memori bahasa pemrograman Java, satu penulisan ke nilai panjang atau ganda yang tidak mudah menguap diperlakukan sebagai dua tulisan yang terpisah: satu hingga masing-masing 32-bit setengah. Ini dapat menghasilkan situasi di mana utas melihat 32 bit pertama dari nilai 64-bit dari satu tulisan, dan 32 bit kedua dari penulisan lain. Menulis dan membaca nilai panjang dan ganda yang mudah menguap selalu atom. Menulis ke dan membaca referensi selalu atom, terlepas dari apakah mereka diimplementasikan sebagai nilai 32-bit atau 64-bit. Beberapa implementasi mungkin merasa nyaman untuk membagi tindakan menulis tunggal pada nilai panjang 64-bit atau ganda menjadi dua tindakan menulis pada nilai 32-bit yang berdekatan. Demi efisiensi, perilaku ini khusus implementasi; Implementasi mesin virtual Java bebas untuk melakukan penulisan ke nilai panjang dan ganda secara atom atau dalam dua bagian. Implementasi mesin virtual Java didorong untuk menghindari pemisahan nilai 64-bit jika memungkinkan. Pemrogram didorong untuk menyatakan nilai 64-bit bersama sebagai volatil atau menyinkronkan program mereka dengan benar untuk menghindari kemungkinan persekutuan.
Isi bagian ini kira -kira mirip dengan apa yang saya jelaskan sebelumnya. Karena operasi dua tipe data panjang dan ganda dapat dibagi menjadi dua bagian: tinggi 32 bit dan 32 bit rendah, tipe panjang atau ganda biasa mungkin bukan atom. Oleh karena itu, setiap orang didorong untuk mengatur variabel panjang dan ganda bersama ke jenis yang mudah menguap, yang dapat memastikan bahwa operasi baca/penulisan tunggal panjang dan ganda adalah atom dalam kasus apa pun.
Ada masalah bahwa variabel yang mudah menguap menjamin atomisitas, yang mudah disalahpahami. Sekarang kami akan menunjukkan masalah ini melalui program berikut:
paket com.paddx.test.concurrent; kelas publik volatiletest01 {volatile int i; public void addi () {i ++; } public static void main (string [] args) melempar interruptedException {final volatiletest01 test01 = volatiletest. () baru; untuk (int n = 0; n <1000; n ++) {thread baru (runnable baru () {@Override public void run () {coba {thread.sleep (10);} catch (interruptedException e) {e.printstacktrace ();} test01.addi ();}} (E.PrintStacktrace ();} test01.addi ();} (). } Thread.sleep (10000); // tunggu selama 10 detik untuk memastikan bahwa eksekusi program di atas selesai system.out.println (test01.i); }}Anda mungkin secara keliru percaya bahwa setelah menambahkan kata kunci yang mudah menguap ke variabel i, program ini aman-aman. Anda dapat mencoba menjalankan program di atas. Berikut adalah hasil dari menjalankan lokal saya:
Mungkin semua orang menjalankan hasil secara berbeda. Namun, harus dilihat bahwa volatil tidak dapat menjamin atomisitas (jika tidak hasilnya harus 1000). Alasannya juga sangat sederhana. i ++ sebenarnya adalah operasi gabungan, termasuk tiga langkah:
(1) Baca nilai i.
(2) Tambahkan 1 ke i.
(3) Tuliskan nilai saya kembali ke memori.
Tidak ada jaminan bahwa ketiga operasi ini bersifat atom. Kami dapat memastikan atomisitas operasi +1 melalui atomicinteger atau disinkronkan.
Catatan: Metode thread.sleep () dieksekusi di banyak tempat di bagian kode di atas, dengan tujuan meningkatkan kemungkinan masalah konkurensi dan tidak memiliki efek lain.
3. Prinsip volatile
Melalui contoh -contoh di atas, kita pada dasarnya harus tahu apa itu volatile dan bagaimana menggunakannya. Sekarang mari kita lihat bagaimana lapisan yang mendasari volatile diimplementasikan.
1. Implementasi visibilitas:
Seperti yang disebutkan dalam artikel sebelumnya, utas itu sendiri tidak secara langsung berinteraksi dengan data memori utama, tetapi melengkapi operasi yang sesuai melalui memori kerja utas. Ini juga merupakan alasan penting mengapa data antara utas tidak terlihat. Oleh karena itu, untuk mencapai visibilitas variabel volatil, Anda dapat mulai langsung dari aspek ini. Ada dua perbedaan utama antara operasi penulisan pada variabel volatil dan variabel biasa:
(1) Saat memodifikasi variabel volatil, nilai yang dimodifikasi akan dipaksa untuk menyegarkan memori utama.
(2) Memodifikasi variabel volatil akan menyebabkan nilai variabel yang sesuai dalam memori kerja utas lain gagal. Oleh karena itu, saat membaca nilai variabel ini lagi, Anda perlu membaca nilai di memori utama lagi.
Melalui dua operasi ini, masalah visibilitas variabel volatil dapat diselesaikan.
2. Implementasi tertib:
Sebelum menjelaskan masalah ini, mari kita pahami aturan yang terjadi sebelumnya di Java. Definisi terjadi sebelum JSR 133 adalah sebagai berikut:
Dua tindakan dapat diperintahkan oleh hubungan yang terjadi sebelumnya. Jika satu tindakan terjadi sebelum yang lain, maka yang pertama terlihat dan dipesan sebelum yang kedua.
Dalam istilah awam, jika terjadi sebelum B, operasi apa pun yang dapat dilihat oleh b. (Setiap orang harus mengingat hal ini, karena kata itu terjadi----dengan mudah disalahpahami seperti sebelum dan sesudah waktu). Mari kita lihat apa yang terjadi sebelum aturan yang didefinisikan dalam JSR 133:
• Setiap tindakan dalam utas terjadi sebelum setiap tindakan selanjutnya di utas itu. • Buka kunci pada monitor terjadi sebelum setiap kunci berikutnya pada monitor itu. • Penulisan ke bidang yang mudah menguap terjadi sebelum setiap bacaan selanjutnya dari volatile itu. • Panggilan untuk memulai () pada utas terjadi sebelum tindakan apa pun di utas yang dimulai. • Semua tindakan di utas terjadi sebelum utas lain berhasil kembali dari gabungan () di utas itu. • Jika suatu tindakan A terjadi sebelum tindakan B, dan B terjadi sebelum tindakan C, maka A terjadi sebelum c.
Diterjemahkan sebagai:
• Operasi sebelumnya terjadi sebelum di utas yang sama. (mis., Dalam satu utas, legal untuk dieksekusi dalam urutan kode. Namun, kompiler dan prosesor dapat menyusun ulang tanpa mempengaruhi hasil eksekusi dalam satu lingkungan berulir.
• Buka operasi pada monitor terjadi sebelum operasi penguncian berikutnya. (Aturan yang disinkronkan)
• Tulis operasi ke variabel volatile terjadi sebelum operasi baca berikutnya. (Aturan yang mudah menguap)
• Metode start () dari utas terjadi sebelum semua operasi utas selanjutnya. (Aturan Mulai Thread)
• Semua operasi utas terjadi sebelum utas lain hubungi bergabung di utas ini dan kembalikan operasi yang berhasil.
• Jika A terjadi sebelum B, b terjadi sebelum C, maka A terjadi sebelum C (transitif).
Di sini kita terutama melihat aturan ketiga: aturan untuk memastikan ketertiban variabel yang mudah menguap. Artikel "Pemrograman Konkurensi Java: Teori Inti" menyebutkan bahwa penataan ulang dibagi menjadi pemesanan ulang kompiler dan pemesanan ulang prosesor. Untuk mengimplementasikan semantik memori yang mudah menguap, JMM membatasi penataan ulang kedua jenis variabel volatil ini. Berikut ini adalah tabel aturan pemesanan ulang yang ditentukan oleh JMM untuk variabel yang mudah menguap:
| Dapat memesan ulang | Operasi ke -2 | |||
| Operasi pertama | Beban normal Toko normal | Beban yang mudah menguap | Toko yang mudah menguap | |
| Beban normal Toko normal | TIDAK | |||
| Beban yang mudah menguap | TIDAK | TIDAK | TIDAK | |
| Toko yang mudah menguap | TIDAK | TIDAK | ||
3. Penghalang Memori
Untuk menerapkan visibilitas yang mudah menguap dan terjadi semantik. JVM yang mendasari dilakukan melalui sesuatu yang disebut "penghalang memori". Penghalang memori, juga dikenal sebagai pagar memori, adalah seperangkat instruksi prosesor yang digunakan untuk menerapkan pembatasan berurutan pada operasi memori. Berikut adalah penghalang memori yang diperlukan untuk menyelesaikan aturan di atas:
| Hambatan yang diperlukan | Operasi ke -2 | |||
| Operasi pertama | Beban normal | Toko normal | Beban yang mudah menguap | Toko yang mudah menguap |
| Beban normal | Loadstore | |||
| Toko normal | Storestore | |||
| Beban yang mudah menguap | Beban | Loadstore | Beban | Loadstore |
| Toko yang mudah menguap | StoreLoad | Storestore | ||
(1) Penghalang beban
Pesanan Eksekusi: Load1―> Loadload -> Load2
Pastikan bahwa LOAD2 dan instruksi beban selanjutnya dapat mengakses data yang dimuat dengan LOAD1 sebelum memuat data.
(2) Storestore Barrier
Pesanan Eksekusi: Store1 -> StoreStore -> Store2
Pastikan bahwa data operasi Store1 terlihat oleh prosesor lain sebelum Store2 dan instruksi toko selanjutnya dijalankan.
(3) LoadStore Barrier
Pesanan Eksekusi: Load1―> LoadStore -> Store2
Pastikan bahwa sebelum Store2 dan instruksi toko selanjutnya dijalankan, data yang dimuat oleh LOAD1 dapat diakses.
(4) StoreLoad Barrier
Pesanan Eksekusi: Store1 -> StoreLoad - load2
Pastikan bahwa sebelum Load2 dan instruksi beban selanjutnya dibaca, data Store1 terlihat oleh prosesor lain.
Akhirnya, saya dapat menggunakan contoh untuk menggambarkan bagaimana penghalang memori dimasukkan ke dalam JVM:
paket com.paddx.test.concurrent; Public Class MemoryBarrier {int a, b; volatile int v, u; void f () {int i, j; i = a; j = b; i = v; // Muat beban j = u; // LoadStore a = i; b = j; // Storestore v = i; // Storestore u = j; // StoreLoad i = u; // loadload // loadstore j = b; a = i; }}4. Ringkasan
Secara keseluruhan, pemahaman yang mudah menguap masih relatif sulit. Jika Anda tidak memahaminya dengan baik, Anda tidak perlu bergegas. Dibutuhkan proses untuk sepenuhnya memahaminya. Anda juga akan melihat skenario penggunaan volatile berkali -kali dalam artikel berikutnya. Di sini saya memiliki pemahaman dasar tentang pengetahuan dasar tentang volatile dan yang asli. Secara umum, volatile adalah optimasi dalam pemrograman bersamaan, yang dapat menggantikan disinkronkan dalam beberapa skenario. Namun, volatil tidak dapat sepenuhnya menggantikan posisi disinkronkan. Hanya dalam beberapa skenario khusus yang dapat diterapkan volatile. Secara umum, dua kondisi berikut harus dipenuhi pada saat yang sama untuk memastikan keamanan utas di lingkungan yang bersamaan:
(1) Operasi tulis ke variabel tidak tergantung pada nilai saat ini.
(2) Variabel ini tidak termasuk dalam invarian dengan variabel lain.
Artikel di atas tentang pemrograman bersamaan Java: penggunaan volatile dan analisis prinsipnya adalah semua konten yang saya bagikan dengan Anda. Saya harap Anda dapat memberi Anda referensi dan saya harap Anda dapat mendukung wulin.com lebih lanjut.