Banyak teman mungkin pernah mendengar kata kunci yang mudah menguap dan mungkin telah menggunakannya. Sebelum Java 5, itu adalah kata kunci yang kontroversial, karena menggunakannya dalam program sering menghasilkan hasil yang tidak terduga. Hanya setelah Java 5, kata kunci yang mudah menguap mendapatkan kembali vitalitasnya.
Meskipun kata kunci yang mudah menguap secara harfiah sederhana untuk dipahami, tidak mudah untuk menggunakannya dengan baik. Karena kata kunci yang mudah menguap terkait dengan model memori Java, sebelum memberi tahu kunci yang mudah menguap, pertama -tama kita memahami konsep dan pengetahuan yang terkait dengan model memori, kemudian menganalisis prinsip implementasi kata kunci yang mudah menguap, dan akhirnya memberikan beberapa skenario menggunakan kata kunci yang mudah menguap.
Berikut adalah garis besar direktori dari artikel ini:
1. Konsep Terkait Model Memori
Seperti yang kita semua tahu, ketika komputer menjalankan program, setiap instruksi dieksekusi dalam CPU, dan selama pelaksanaan instruksi, itu pasti akan melibatkan membaca dan menulis data. Karena data sementara selama operasi program disimpan dalam memori utama (memori fisik), ada masalah saat ini. Karena kecepatan eksekusi CPU sangat cepat, proses membaca data dari memori dan menulis data ke memori jauh lebih lambat daripada pelaksanaan instruksi CPU. Oleh karena itu, jika operasi data harus dilakukan melalui interaksi dengan memori kapan saja, kecepatan eksekusi instruksi akan sangat berkurang. Oleh karena itu, ada cache di CPU.
Artinya, ketika program sedang berjalan, itu akan menyalin data yang diperlukan oleh operasi dari memori utama ke cache CPU. Kemudian ketika CPU melakukan perhitungan, ia dapat langsung membaca data dari cache dan menulis data kepadanya. Setelah operasi selesai, data dalam cache akan disiram ke dalam memori utama. Mari berikan contoh sederhana, seperti kode berikut:
i = i + 1;
Ketika utas menjalankan pernyataan ini, pertama -tama akan membaca nilai I dari memori utama, kemudian menyalin salinan ke cache, dan kemudian CPU akan menjalankan instruksi untuk menambahkan 1 ke I, kemudian menulis data ke cache, dan akhirnya menyegarkan nilai terbaru I di cache ke memori utama.
Tidak ada masalah dengan kode ini berjalan di satu utas, tetapi akan ada masalah saat berjalan dalam multi-thread. Dalam CPU multi-core, setiap utas dapat berjalan dalam CPU yang berbeda, sehingga setiap utas memiliki cache sendiri saat berjalan (untuk CPU inti tunggal, masalah ini akan benar-benar terjadi, tetapi dieksekusi secara terpisah dalam bentuk penjadwalan utas). Dalam artikel ini, kami mengambil CPU multi-core sebagai contoh.
Misalnya, dua utas menjalankan kode ini secara bersamaan. Jika nilai I adalah 0 di awal, maka kami berharap nilai saya akan menjadi 2 setelah kedua utas dieksekusi. Tapi apakah ini masalahnya?
Mungkin ada salah satu dari situasi berikut: Pada awalnya, dua utas membaca nilai I dan menyimpannya di cache CPU masing -masing, dan kemudian Thread 1 melakukan operasi menambahkan 1, dan kemudian menulis nilai terbaru I ke memori. Pada saat ini, nilai I dalam cache Thread 2 masih 0. Setelah melakukan operasi 1, nilai I adalah 1, dan kemudian Thread 2 menulis nilai I ke memori.
Nilai hasil akhir I adalah 1, bukan 2. Ini adalah masalah konsistensi cache yang terkenal. Variabel ini yang diakses oleh beberapa utas biasanya disebut variabel bersama.
Dengan kata lain, jika suatu variabel di -cache dalam beberapa CPU (biasanya hanya terjadi selama pemrograman multithreading), maka mungkin ada masalah inkonsistensi cache.
Untuk menyelesaikan masalah inkonsistensi cache, biasanya ada dua solusi:
1) Dengan menambahkan kunci# kunci ke bus
2) melalui protokol koherensi cache
Kedua metode ini disediakan di tingkat perangkat keras.
Pada CPU awal, masalah inkonsistensi cache diselesaikan dengan menambahkan kunci# kunci ke bus. Karena komunikasi antara CPU dan komponen lain dilakukan melalui bus, jika bus ditambahkan dengan kunci# kunci, itu berarti bahwa CPU lain diblokir dari mengakses komponen lain (seperti memori), sehingga hanya satu CPU yang dapat menggunakan memori variabel ini. Misalnya, dalam contoh di atas, jika utas mengeksekusi i = i +1, dan jika sinyal kunci LCOK# dikirim pada bus selama pelaksanaan kode ini, maka hanya setelah menunggu kode dieksekusi sepenuhnya, CPU lain dapat membaca variabel dari memori di mana variabel saya berada dan kemudian melakukan operasi yang sesuai. Ini memecahkan masalah inkonsistensi cache.
Tetapi metode di atas akan memiliki masalah, karena CPU lain tidak dapat mengakses memori selama kunci bus, yang mengakibatkan inefisiensi.
Jadi protokol konsistensi cache muncul. Yang paling terkenal adalah protokol MESI Intel, yang memastikan bahwa salinan variabel bersama yang digunakan dalam setiap cache konsisten. Gagasan intinya adalah: ketika CPU menulis data, jika menemukan bahwa variabel yang dioperasikan adalah variabel bersama, yaitu, ada salinan variabel di CPU lain, ia akan memberi sinyal CPU lain untuk mengatur garis cache variabel ke keadaan tidak valid. Oleh karena itu, ketika CPU lain perlu membaca variabel ini dan menemukan bahwa garis cache yang menyimpan variabel dalam cache mereka tidak valid, maka akan dibaca ulang dari memori.
2. Tiga konsep dalam pemrograman bersamaan
Dalam pemrograman bersamaan, kami biasanya menghadapi tiga masalah berikut: masalah atomisitas, masalah visibilitas, dan masalah tertib. Mari kita lihat ketiga konsep ini terlebih dahulu:
1. Atomisitas
Atomisitas: Artinya, satu operasi atau beberapa operasi dieksekusi semua dan proses eksekusi tidak akan terganggu oleh faktor apa pun, atau tidak akan dieksekusi.
Contoh yang sangat klasik adalah masalah transfer rekening bank:
Misalnya, jika Anda mentransfer 1.000 yuan dari akun A ke akun B, itu pasti akan mencakup 2 operasi: kurangi 1.000 yuan dari akun A dan tambahkan 1.000 yuan ke akun B.
Bayangkan saja konsekuensi apa yang akan disebabkan jika kedua operasi ini tidak atom. Jika 1.000 yuan dikurangi dari akun A, operasi akan tiba -tiba diakhiri. Kemudian, 500 yuan ditarik dari B, dan setelah menarik 500 yuan, kemudian operasi menambahkan 1.000 yuan ke akun B. Ini akan mengarah pada fakta bahwa meskipun akun A memiliki minus 1.000 yuan, akun B belum menerima 1.000 yuan yang ditransfer.
Oleh karena itu, kedua operasi ini harus atom untuk memastikan bahwa tidak ada masalah yang tidak terduga.
Apa hasil yang akan tercermin dalam pemrograman bersamaan?
Untuk memberikan contoh paling sederhana, pikirkan apa yang akan terjadi jika proses menetapkan variabel 32-bit bukan atom?
i = 9;
Jika utas menjalankan pernyataan ini, saya akan mengasumsikan bahwa penugasan variabel 32-bit mencakup dua proses: penugasan 16-bit yang lebih rendah dan penugasan 16-bit yang lebih tinggi.
Kemudian suatu situasi dapat terjadi: Ketika nilai 16-bit rendah ditulis, tiba-tiba terganggu, dan pada saat ini utas lain membaca nilai i, lalu apa yang dibaca adalah data yang salah.
2. Visibilitas
Visibilitas mengacu ketika beberapa utas mengakses variabel yang sama, satu utas memodifikasi nilai variabel, dan utas lainnya dapat segera melihat nilai yang dimodifikasi.
Sebagai contoh sederhana, lihat kode berikut:
// Kode yang dieksekusi oleh utas 1 adalah int i = 0; i = 10; // Kode yang dieksekusi oleh utas 2 adalah j = i;
Jika utas eksekusi 1 adalah CPU1 dan utas eksekusi 2 adalah CPU2. Dari analisis di atas, kita dapat melihat bahwa ketika Thread 1 menjalankan kalimat i = 10, nilai awal I akan dimuat ke dalam cache CPU1 dan kemudian menetapkan nilai 10. Kemudian nilai I dalam cache CPU1 menjadi 10, tetapi tidak segera ditulis ke memori utama.
Pada saat ini, Thread 2 mengeksekusi j = i, dan pertama -tama akan pergi ke memori utama untuk membaca nilai I dan memuatnya ke dalam cache CPU2. Perhatikan bahwa nilai I dalam memori masih 0, jadi nilai J akan 0, bukan 10.
Ini adalah masalah visibilitas. Setelah utas 1 memodifikasi variabel i, utas 2 tidak segera melihat nilai yang dimodifikasi oleh utas 1.
3. Pesanan
Pesanan: Artinya, urutan pelaksanaan program dieksekusi dalam urutan kode. Sebagai contoh sederhana, lihat kode berikut:
int i = 0; bendera boolean = false; i = 1; // pernyataan 1 flag = true; // Pernyataan 2
Kode di atas mendefinisikan variabel tipe int, variabel tipe boolean, dan kemudian menetapkan nilai masing-masing ke dua variabel. Dari perspektif urutan kode, Pernyataan 1 adalah sebelum pernyataan 2. Jadi ketika JVM benar -benar menjalankan kode ini, apakah itu akan memastikan bahwa Pernyataan 1 akan dieksekusi sebelum Pernyataan 2? Belum tentu, mengapa? Pesan ulang instruksi dapat terjadi di sini.
Mari kita jelaskan apa itu instruksi ulang. Secara umum, untuk meningkatkan efisiensi operasi program, prosesor dapat mengoptimalkan kode input. Ini tidak memastikan bahwa urutan eksekusi dari setiap pernyataan dalam program konsisten dengan urutan dalam kode, tetapi akan memastikan bahwa hasil eksekusi akhir dari program dan hasil dari urutan eksekusi kode konsisten.
Misalnya, dalam kode di atas, yang menjalankan Pernyataan 1 dan Pernyataan 2 pertama tidak berpengaruh pada hasil program akhir, maka ada kemungkinan bahwa selama proses eksekusi, Pernyataan 2 dieksekusi terlebih dahulu dan Pernyataan 1 dieksekusi nanti.
Tetapi ketahuilah bahwa meskipun prosesor akan menyusun ulang instruksi, itu akan memastikan bahwa hasil akhir dari program akan sama dengan urutan eksekusi kode. Jadi apa jaminannya? Mari kita lihat contoh berikut:
int a = 10; // pernyataan 1int r = 2; // pernyataan 2a = a + 3; // pernyataan 3r = a*a; // Pernyataan 4
Kode ini memiliki 4 pernyataan, jadi kemungkinan pesanan eksekusi adalah:
Jadi apakah mungkin menjadi perintah eksekusi: Pernyataan 2 Pernyataan 1 Pernyataan 4 Pernyataan 3
Itu tidak mungkin karena prosesor akan mempertimbangkan ketergantungan data antara instruksi saat menyusun ulang. Jika instruksi instruksi 2 harus menggunakan hasil instruksi 1, prosesor akan memastikan bahwa instruksi 1 akan dieksekusi sebelum instruksi 2.
Meskipun pemesanan ulang tidak akan mempengaruhi hasil eksekusi program dalam satu utas, bagaimana dengan multithreading? Mari kita lihat contoh di bawah ini:
// Thread 1: Context = LoadContext (); // Nyatakan 1Inited = true; // status 2 // utas 2: while (! Inited) {sleep ()} dosomethingwithconfig (konteks);Dalam kode di atas, karena pernyataan 1 dan 2 tidak memiliki dependensi data, mereka dapat dipesan ulang. Jika terjadi pemesanan ulang, Pernyataan 2 pertama kali dieksekusi selama eksekusi Thread 1, dan ini adalah Thread 2 akan berpikir bahwa pekerjaan inisialisasi telah selesai, dan kemudian akan melompat keluar dari loop sementara untuk menjalankan metode dosomethingwithConfig (konteks). Pada saat ini, konteksnya tidak diinisialisasi, yang akan menyebabkan kesalahan program.
Seperti yang dapat dilihat dari hal di atas, penataan ulang instruksi tidak akan mempengaruhi eksekusi satu utas, tetapi akan mempengaruhi kebenaran eksekusi utas secara bersamaan.
Dengan kata lain, untuk melaksanakan program bersamaan dengan benar, atomisitas, visibilitas dan ketertiban harus dipastikan. Selama seseorang tidak dijamin, itu dapat menyebabkan program berjalan secara tidak benar.
3. Model Memori Java
Saya berbicara tentang beberapa masalah yang mungkin muncul dalam model memori dan pemrograman bersamaan. Mari kita lihat model memori Java dan pelajari jaminan apa yang disediakan oleh model memori Java untuk kita dan metode dan mekanisme apa yang disediakan di Java untuk memastikan kebenaran eksekusi program saat melakukan pemrograman multi-utas.
Dalam spesifikasi mesin virtual Java, ia berusaha untuk mendefinisikan model memori Java (JMM) untuk memblokir perbedaan akses memori antara berbagai platform perangkat keras dan sistem operasi, sehingga memungkinkan program Java untuk mencapai efek akses memori yang konsisten pada berbagai platform. Jadi apa yang diatur oleh model memori Java? Ini mendefinisikan aturan akses untuk variabel dalam suatu program. Singkatnya, ini mendefinisikan urutan eksekusi program. Perhatikan bahwa untuk mendapatkan kinerja eksekusi yang lebih baik, model memori Java tidak membatasi mesin eksekusi dari menggunakan register atau cache prosesor untuk meningkatkan kecepatan eksekusi instruksi, juga tidak membatasi kompiler untuk memesan ulang instruksi. Dengan kata lain, dalam model memori Java, juga akan ada masalah konsistensi cache dan masalah pemesanan ulang instruksi.
Model memori Java menetapkan bahwa semua variabel berada dalam memori utama (mirip dengan memori fisik yang disebutkan di atas), dan setiap utas memiliki memori kerjanya sendiri (mirip dengan cache sebelumnya). Semua operasi utas pada variabel harus dilakukan dalam memori kerja, dan tidak dapat secara langsung beroperasi pada memori utama. Dan setiap utas tidak dapat mengakses memori kerja utas lain.
Untuk memberikan contoh sederhana: di Java, jalankan pernyataan berikut:
i = 10;
Utas eksekusi pertama -tama harus menetapkan garis cache di mana variabel I berada di utas kerjanya sendiri, dan kemudian menulisnya ke memori utama. Alih -alih menulis nilai 10 langsung ke memori utama.
Jadi jaminan apa yang diberikan bahasa Java sendiri untuk atomisitas, visibilitas, dan ketertiban?
1. Atomisitas
Di Java, operasi baca dan penugasan variabel tipe data dasar adalah operasi atom, yaitu operasi ini tidak dapat terganggu dan dieksekusi atau tidak.
Meskipun kalimat di atas tampaknya sederhana, itu tidak mudah dimengerti. Lihat contoh berikut i:
Harap analisis mana dari operasi berikut ini adalah operasi atom:
x = 10; // pernyataan 1y = x; // pernyataan 2x ++; // pernyataan 3x = x + 1; // Pernyataan 4
Sekilas, beberapa teman mungkin mengatakan bahwa operasi dalam empat pernyataan di atas adalah semua operasi atom. Faktanya, hanya pernyataan 1 yang merupakan operasi atom, dan tidak satu pun dari tiga pernyataan lainnya yang merupakan operasi atom.
Pernyataan 1 secara langsung menetapkan nilai 10 ke x, yang berarti bahwa utas menjalankan pernyataan ini dan menulis nilai 10 langsung ke dalam memori yang berfungsi.
Pernyataan 2 sebenarnya berisi 2 operasi. Pertama -tama perlu membaca nilai x, dan kemudian tulis nilai X ke memori kerja. Meskipun dua operasi membaca nilai X dan menulis nilai X ke memori kerja adalah operasi atom, mereka bukan operasi atom bersama.
Demikian pula, x ++ dan x = x+1 termasuk 3 operasi: Baca nilai x, lakukan operasi menambahkan 1, dan tulis nilai baru.
Oleh karena itu, hanya pengoperasian pernyataan 1 dalam empat pernyataan di atas adalah atom.
Dengan kata lain, hanya bacaan dan penugasan sederhana (dan nomor tersebut harus ditetapkan ke variabel, dan penugasan timbal balik antar variabel bukanlah operasi atom) adalah operasi atom.
Namun, ada satu hal yang perlu diperhatikan di sini: di bawah platform 32-bit, pembacaan dan penugasan data 64-bit perlu diselesaikan melalui dua operasi, dan atomisitasnya tidak dapat dijamin. Namun, tampaknya dalam JDK terbaru, JVM telah memastikan bahwa membaca dan penugasan data 64-bit juga merupakan operasi atom.
Dari atas, dapat dilihat bahwa model memori Java hanya memastikan bahwa bacaan dan penugasan dasar adalah operasi atom. Jika Anda ingin mencapai atomisitas dari beragam operasi, itu dapat dicapai melalui sinkronisasi dan kunci. Karena disinkronkan dan LOCK dapat memastikan bahwa hanya satu utas yang menjalankan blok kode kapan saja, secara alami tidak akan ada masalah atomisitas, sehingga memastikan atomisitas.
2. Visibilitas
Untuk visibilitas, Java menyediakan kata kunci yang mudah menguap untuk memastikan visibilitas.
Ketika variabel bersama dimodifikasi dengan volatile, itu memastikan bahwa nilai yang dimodifikasi akan segera diperbarui ke memori utama, dan ketika utas lain perlu membacanya, itu akan membaca nilai baru dalam memori.
Namun, variabel bersama biasa tidak dapat menjamin visibilitas, karena tidak pasti kapan variabel bersama normal ditulis ke memori utama setelah dimodifikasi. Ketika utas lain membacanya, nilai lama asli mungkin masih ada dalam memori, sehingga visibilitas tidak dapat dijamin.
Selain itu, sinkronisasi dan kunci juga dapat memastikan visibilitas. Sinkronisasi dan kunci dapat memastikan bahwa hanya satu utas yang memperoleh kunci pada waktu yang sama dan menjalankan kode sinkronisasi. Sebelum melepaskan kunci, modifikasi variabel akan disegarkan ke memori utama. Oleh karena itu, visibilitas dapat dijamin.
3. Pesanan
Dalam model memori Java, kompiler dan prosesor diizinkan untuk memesan ulang instruksi, tetapi proses pemesanan ulang tidak akan mempengaruhi pelaksanaan program tunggal, tetapi akan mempengaruhi kebenaran eksekusi bersamaan multi-threaded.
Di Java, "garis pesanan" tertentu dapat dipastikan melalui kata kunci yang mudah menguap (prinsip spesifik dijelaskan di bagian selanjutnya). Selain itu, sinkronisasi dan kunci dapat digunakan untuk memastikan pesanan. Jelas, disinkronkan dan kunci memastikan bahwa ada utas yang menjalankan kode sinkronisasi pada setiap momen, yang setara dengan membiarkan utas menjalankan kode sinkronisasi secara berurutan, yang secara alami memastikan pesanan.
Selain itu, model memori Java memiliki "garis pesanan" bawaan, yaitu, dapat dijamin tanpa cara apa pun, yang biasanya disebut prinsip yang terjadi sebelum. Jika urutan eksekusi dari dua operasi tidak dapat diturunkan dari prinsip yang terjadi sebelum, maka mereka tidak dapat menjamin ketertiban dan mesin virtual mereka dapat memesan ulang sesuka hati.
Mari kita perkenalkan prinsip yang terjadi sebelum (prinsip kejadian prioritas):
8 prinsip ini dikutip dari "pemahaman mendalam tentang mesin virtual Java".
Di antara 8 aturan ini, 4 aturan pertama lebih penting, sedangkan 4 aturan terakhir semuanya jelas.
Mari kita jelaskan 4 aturan pertama di bawah ini:
Untuk aturan pesanan program, pemahaman saya adalah bahwa pelaksanaan sepotong kode program tampaknya dipesan dalam satu utas. Perhatikan bahwa meskipun aturan ini menyebutkan bahwa "operasi yang ditulis di depan terjadi terlebih dahulu dalam operasi yang ditulis di belakang", ini harus menjadi urutan di mana program tampaknya dieksekusi dalam urutan kode, karena mesin virtual dapat memesan ulang kode program yang diinstruksikan. Meskipun pemesanan ulang dilakukan, hasil eksekusi akhir konsisten dengan eksekusi sekuensial program, dan hanya akan memesan ulang instruksi yang tidak memiliki dependensi data. Oleh karena itu, dalam satu utas, eksekusi program tampaknya dieksekusi secara tertib, yang harus dipahami dengan hati -hati. Faktanya, aturan ini digunakan untuk memastikan kebenaran hasil eksekusi program dalam satu utas, tetapi tidak dapat menjamin kebenaran program dengan cara multi-threaded.
Aturan kedua juga lebih mudah dipahami, yaitu, jika kunci yang sama dalam keadaan terkunci, harus dilepaskan sebelum operasi kunci dapat dilanjutkan.
Aturan ketiga adalah aturan yang relatif penting dan juga apa yang akan dibahas nanti. Secara intuitif, jika utas menulis variabel terlebih dahulu dan kemudian utas berbunyi, maka operasi tulis pasti akan terjadi terlebih dahulu dalam operasi baca.
Aturan keempat sebenarnya mencerminkan bahwa prinsip yang terjadi sebelumnya adalah transitif.
4. Analisis mendalam dari kata kunci yang mudah menguap
Saya telah membicarakan banyak hal sebelumnya, tetapi mereka sebenarnya membuka jalan untuk memberi tahu kata kunci yang mudah menguap, jadi mari kita mulai ke topik.
1. Semantik dua lapis dari kata kunci yang mudah menguap
Setelah variabel bersama (variabel anggota kelas, variabel anggota statis kelas) dimodifikasi dengan mudah menguap, ia memiliki dua lapisan semantik:
1) Pastikan visibilitas utas yang berbeda saat mengoperasikan variabel ini, yaitu satu utas memodifikasi nilai variabel tertentu, dan nilai baru ini segera terlihat oleh utas lain.
2) dilarang untuk memesan ulang instruksi.
Mari kita lihat sepotong kode terlebih dahulu. Jika utas 1 dieksekusi terlebih dahulu dan utas 2 dieksekusi nanti:
// Thread 1Boolean stop = false; while (! stop) {dosomething ();} // thread 2stop = true;Kode ini adalah kode yang sangat khas, dan banyak orang dapat menggunakan metode markup ini saat mengganggu utas. Namun pada kenyataannya, akankah kode ini berjalan dengan benar? Apakah utas akan terganggu? Belum tentu. Mungkin sebagian besar waktu, kode ini dapat mengganggu utas, tetapi juga dapat menyebabkan utas tidak terganggu (meskipun kemungkinan ini sangat kecil, begitu ini terjadi, itu akan menyebabkan loop mati).
Mari kita jelaskan mengapa kode ini dapat menyebabkan utas gagal mengganggu. Seperti yang dijelaskan sebelumnya, setiap utas memiliki memori kerjanya sendiri selama operasi, jadi ketika utas 1 sedang berjalan, ia akan menyalin nilai variabel berhenti dan memasukkannya ke dalam memori kerjanya sendiri.
Kemudian ketika Thread 2 mengubah nilai variabel berhenti, tetapi belum punya waktu untuk menulisnya ke memori utama, Thread 2 pergi untuk melakukan hal -hal lain, maka Thread 1 tidak tahu tentang perubahan Thread 2 ke variabel berhenti, sehingga akan terus melingkar.
Tetapi setelah memodifikasi dengan volatile itu menjadi berbeda:
Pertama: menggunakan kata kunci yang mudah menguap akan memaksa nilai yang dimodifikasi untuk segera ditulis ke memori utama;
Kedua: jika Anda menggunakan kata kunci yang mudah menguap, ketika Thread 2 memodifikasinya, garis cache dari variabel cache berhenti di memori kerja Thread 1 akan tidak valid (jika tercermin dalam lapisan perangkat keras, garis cache yang sesuai dalam cache L1 atau L2 dari CPU tidak valid);
Ketiga: Karena garis cache dari variabel cache berhenti dalam memori kerja Thread 1 tidak valid, utas 1 akan membacanya di memori utama ketika membaca nilai variabel berhenti lagi.
Kemudian ketika Thread 2 memodifikasi nilai berhenti (tentu saja, ada 2 operasi di sini, memodifikasi nilai dalam memori kerja Thread 2, dan kemudian menulis nilai yang dimodifikasi ke memori), garis cache dari variabel cache berhenti di memori kerja Thread 1 akan tidak valid. Ketika Thread 1 membaca, ia menemukan bahwa garis cache tidak valid. Ini akan menunggu alamat memori utama yang sesuai dari jalur cache diperbarui, dan kemudian membaca nilai terbaru di memori utama yang sesuai.
Lalu apa yang dibaca utas 1 adalah nilai yang benar terbaru.
2. Apakah volatile menjamin atomisitas?
Dari atas, kita tahu bahwa kata kunci yang mudah menguap memastikan visibilitas operasi, tetapi dapatkah volatile memastikan bahwa operasi pada variabel adalah atom?
Mari kita lihat contoh di bawah ini:
tes kelas publik {public volatile int inc = 0; public void peningkatan () {Inc ++; } public static void main (string [] args) {final test test = new test (); untuk (int i = 0; i <10; i ++) {thread baru () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.awal(); } while (thread.activeCount ()> 1) // Pastikan utas sebelumnya telah menyelesaikan thread.yield (); System.out.println (test.inc); }}Pikirkan apa hasil output dari program ini? Mungkin beberapa teman berpikir itu 10.000. Tetapi pada kenyataannya, menjalankannya akan menemukan bahwa hasil dari setiap menjalankan tidak konsisten, dan itu adalah angka kurang dari 10.000.
Beberapa teman mungkin memiliki pertanyaan, itu salah. Di atas adalah untuk melakukan operasi pendakian diri pada variabel inc. Karena volatile memastikan visibilitas, setelah kenaikan diri Inc di setiap utas, nilai yang dimodifikasi dapat dilihat di utas lain. Oleh karena itu, 10 utas telah melakukan 1000 operasi masing -masing, sehingga nilai akhir INC harus 1000*10 = 10000.
Ada kesalahpahaman di sini. Kata kunci yang mudah menguap dapat memastikan visibilitas, tetapi program di atas salah karena tidak dapat menjamin atomisitas. Visibilitas hanya dapat memastikan bahwa nilai terbaru dibaca setiap saat, tetapi volatile tidak dapat menjamin atomisitas pengoperasian variabel.
Seperti yang disebutkan sebelumnya, operasi pendakian otomatis bukan atom. Ini termasuk membaca nilai asli dari suatu variabel, melakukan operasi tambahan, dan menulis ke memori yang berfungsi. Dengan kata lain, tiga sub-operasi dari operasi pendakian diri dapat dilakukan secara terpisah, yang dapat mengarah pada situasi berikut:
Jika nilai variabel Inc pada waktu tertentu adalah 10,
Thread 1 melakukan operasi pendakian diri pada variabel. Thread 1 pertama kali membaca nilai asli dari variabel Inc, dan kemudian utas 1 diblokir;
Kemudian Thread 2 melakukan operasi pendakian diri pada variabel, dan Thread 2 juga membaca nilai asli dari variabel Inc. Karena utas 1 hanya melakukan operasi baca pada variabel Inc dan tidak memodifikasi variabel, itu tidak akan menyebabkan garis cache dari variabel cache cache inc in thread 2 menjadi tidak valid. Oleh karena itu, Thread 2 akan langsung menuju ke memori utama untuk membaca nilai Inc. Ketika ditemukan bahwa nilai Inc adalah 10, kemudian melakukan operasi menambahkan 1, dan menulis 11 ke memori kerja, dan akhirnya menulisnya ke memori utama.
Kemudian utas 1 lalu lakukan operasi penambahan. Karena nilai Inc telah dibaca, perhatikan bahwa nilai Inc dalam Thread 1 masih 10 saat ini, jadi setelah Thread 1 menambahkan Inc, nilai Inc adalah 11, kemudian menulis 11 untuk bekerja memori, dan akhirnya menulisnya ke memori utama.
Kemudian setelah kedua utas melakukan operasi penumpang diri, Inc hanya meningkat sebesar 1.
Setelah menjelaskan hal ini, beberapa teman mungkin memiliki pertanyaan, itu salah. Bukankah dijamin bahwa suatu variabel akan membatalkan garis cache saat memodifikasi variabel volatile? Maka utas lain akan membaca nilai baru. Ya, ini benar. Ini adalah aturan variabel yang mudah menguap dalam aturan yang terjadi sebelum di atas, tetapi harus dicatat bahwa jika utas 1 membaca variabel dan diblokir, nilai INC tidak akan dimodifikasi. Kemudian meskipun volatile dapat memastikan bahwa Thread 2 membaca nilai variabel Inc dari memori, Thread 1 belum memodifikasinya, jadi Thread 2 tidak akan melihat nilai yang dimodifikasi sama sekali.
Penyebab akar adalah bahwa operasi autoINCREMENT bukan operasi atom, dan volatil tidak dapat menjamin bahwa operasi apa pun pada variabel adalah atom.
Ubah kode di atas ke salah satu dari yang berikut ini dapat mencapai efek:
Gunakan sinkronisasi:
tes kelas publik {public int inc = 0; peningkatan void yang disinkronkan publik () {Inc ++; } public static void main (string [] args) {final test test = new test (); untuk (int i = 0; i <10; i ++) {thread baru () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.awal(); } while (thread.activeCount ()> 1) // Pastikan utas sebelumnya telah menyelesaikan thread.yield (); System.out.println (test.inc); }} Menggunakan kunci:
tes kelas publik {public int inc = 0; Lock lock = baru reentrantlock (); public void peningkatan () {lock.lock (); coba {Inc ++; } akhirnya {lock.unlock (); }} public static void main (string [] args) {final test test = new test (); untuk (int i = 0; i <10; i ++) {thread baru () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.awal(); } while (thread.activeCount ()> 1) // Pastikan bahwa utas sebelumnya telah dieksekusi thread.yield (); System.out.println (test.inc); }} Menggunakan AtomicInteger:
tes kelas publik {public atomicinteger inc = atomicinteger baru (); public void peningkatan () {inc.getAndIncrement (); } public static void main (string [] args) {final test test = new test (); untuk (int i = 0; i <10; i ++) {thread baru () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.awal(); } while (thread.activeCount ()> 1) // Pastikan bahwa utas sebelumnya telah dieksekusi thread.yield (); System.out.println (test.inc); }}Beberapa kelas operasi atom disediakan di bawah java.util.concurrent.atomic Paket Java 1.5, yaitu, pendakian diri (tambahkan 1 operasi), penangguhan diri (tambahkan 1 operasi), operasi penambahan (tambahkan angka), dan operasi pengurangan (tambahkan angka) dari jenis data dasar untuk memastikan bahwa operasi ini adalah operasi atom. Atomik menggunakan CAS untuk mengimplementasikan operasi atom (bandingkan dan bertukar). CAS sebenarnya diimplementasikan menggunakan instruksi CMPXCHG yang disediakan oleh prosesor, dan prosesor menjalankan instruksi CMPXCHG adalah operasi atom.
3. Dapat volatile memastikan keteraturan?
Seperti yang disebutkan sebelumnya, kata kunci yang mudah menguap dapat melarang pemesanan ulang instruksi, sehingga volatile dapat memastikan pesanan sampai batas tertentu.
Ada dua makna yang dilarang menyusun ulang kata kunci yang mudah menguap:
1) Ketika program menjalankan operasi baca atau tulis dari variabel yang mudah menguap, semua perubahan pada operasi sebelumnya harus dilakukan, dan hasilnya sudah terlihat oleh operasi selanjutnya; Operasi selanjutnya harus belum dilakukan;
2) Saat melakukan optimasi instruksi, pernyataan yang diakses ke variabel volatil tidak dapat ditempatkan di belakangnya, juga tidak dapat pernyataan yang mengikuti variabel volatile ditempatkan di depannya.
Mungkin apa yang dikatakan di atas agak membingungkan, jadi berikan contoh sederhana:
// x dan y adalah variabel non-volatile // flag adalah variabel volatil x = 2; // pernyataan 1y = 0; // pernyataan 2Flag = true; // pernyataan 3x = 4; // pernyataan 4y = -1; // Pernyataan 5
Karena variabel bendera adalah variabel yang mudah menguap, ketika melakukan proses pemesanan ulang instruksi, Pernyataan 3 tidak akan ditempatkan sebelum Pernyataan 1 dan 2, juga tidak akan ditempatkan setelah Pernyataan 3, dan Pernyataan 4 dan 5. Namun, tidak dijamin bahwa urutan Pernyataan 1 dan Pernyataan 2 dan Urutan Pernyataan 4 dan Pernyataan 5 tidak dijamin.
Selain itu, kata kunci yang mudah menguap dapat memastikan bahwa ketika pernyataan 3 dieksekusi, Pernyataan 1 dan Pernyataan 2 harus dieksekusi, dan hasil eksekusi dari Pernyataan 1 dan Pernyataan 2 terlihat oleh Pernyataan 3, Pernyataan 4, dan Pernyataan 5.
Jadi mari kita kembali ke contoh sebelumnya:
// Thread 1: Context = LoadContext (); // Nyatakan 1Inited = true; // status 2 // utas 2: while (! Inited) {sleep ()} dosomethingwithconfig (konteks);Ketika saya memberikan contoh ini, saya menyebutkan bahwa ada kemungkinan bahwa Pernyataan 2 akan dieksekusi sebelum Pernyataan 1, begitu lama dapat menyebabkan konteksnya tidak diinisialisasi, dan Thread 2 menggunakan konteks yang tidak diinisialisasi untuk beroperasi, menghasilkan kesalahan program.
Jika variabel inited dimodifikasi dengan kata kunci yang mudah menguap, masalah ini tidak akan terjadi, karena ketika pernyataan 2 dieksekusi, itu pasti akan memastikan bahwa konteksnya telah diinisialisasi.
4. Prinsip dan mekanisme implementasi volatile
Deskripsi sebelumnya dari beberapa penggunaan kata kunci yang mudah menguap berasal dari. Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}参考资料:
《Java编程思想》
《深入理解Java虚拟机》
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。