Multithreading. Model memori Java (bagian 2)

Halo, Habr! Untuk perhatian Anda, saya persembahkan terjemahan bagian kedua dari artikel "Model Memori Java" oleh Jakob Jenkov. Bagian pertama ada di sini .



Arsitektur perangkat keras memori



Arsitektur perangkat keras memori modern agak berbeda dari model memori Java internal. Penting untuk memahami arsitektur perangkat keras untuk memahami bagaimana model Java bekerja dengannya. Bagian ini menjelaskan arsitektur perangkat keras memori umum, dan bagian berikutnya menjelaskan cara kerja Java dengannya.



Berikut adalah diagram yang disederhanakan dari arsitektur perangkat keras komputer modern:



Komputer modern biasanya memiliki 2 prosesor atau lebih. Beberapa prosesor ini mungkin juga memiliki banyak inti. Pada komputer seperti itu, dimungkinkan untuk menjalankan beberapa utas secara bersamaan. Setiap prosesor (catatan penerjemah - selanjutnya, penulis mungkin berarti inti prosesor atau prosesor inti tunggal oleh prosesor)dapat menjalankan satu utas pada waktu tertentu. Ini berarti bahwa jika aplikasi Java Anda multithread, maka dalam program Anda, satu thread dapat berjalan secara bersamaan per prosesor.



Setiap prosesor berisi satu set register yang pada dasarnya ada di memorinya. Ia dapat melakukan operasi pada data di register jauh lebih cepat daripada pada data yang ada di memori utama (RAM) komputer. Ini karena prosesor dapat mengakses register ini lebih cepat.



Setiap CPU juga dapat memiliki lapisan cache. Faktanya, sebagian besar prosesor modern memilikinya. Prosesor dapat mengakses memori cache jauh lebih cepat daripada memori utama, tetapi umumnya tidak secepat register internalnya. Dengan demikian, kecepatan akses ke memori cache berada di antara kecepatan akses ke register internal dan ke memori utama. Beberapa prosesor mungkin memiliki cache berjenjang, tetapi ini tidak penting untuk dipahami untuk memahami bagaimana model memori Java berinteraksi dengan memori perangkat keras. Penting untuk diketahui bahwa prosesor dapat memiliki beberapa level memori cache.



Komputer juga berisi area memori utama (RAM). Semua prosesor dapat mengakses memori utama. Area memori utama biasanya jauh lebih besar daripada cache prosesor.



Biasanya, ketika prosesor membutuhkan akses ke memori utama, prosesor membaca sebagian darinya ke dalam memori cache. Itu juga dapat membaca beberapa data dari cache ke register internalnya dan kemudian melakukan operasi padanya. Ketika CPU perlu menulis hasil kembali ke memori utama, ia membuang data dari register internalnya ke memori cache dan pada titik tertentu ke memori utama.



Data yang disimpan dalam cache biasanya dialihkan kembali ke memori utama saat prosesor perlu menyimpan sesuatu yang lain di cache. Cache dapat menghapus memorinya dan menulis data baru ke dalamnya pada saat yang bersamaan. Prosesor tidak harus membaca / menulis cache lengkap setiap kali diperbarui. Biasanya cache diperbarui dalam blok kecil memori yang disebut "baris cache". Satu atau lebih baris cache dapat dibaca ke dalam memori cache, dan satu atau lebih baris cache dapat dipindahkan kembali ke memori utama.



Menggabungkan model memori Java dan arsitektur memori perangkat keras



Seperti yang disebutkan, model memori Java dan arsitektur perangkat keras memori berbeda. Arsitektur perangkat keras tidak membedakan antara tumpukan benang dan tumpukan. Pada perangkat keras, tumpukan benang dan tumpukan berada di memori utama. Porsi tumpukan dan tumpukan utas terkadang ada di cache dan register internal CPU. Hal ini ditunjukkan pada diagram:



Ketika objek dan variabel dapat disimpan di berbagai area memori komputer, masalah tertentu dapat muncul. Ada dua yang utama:

• Visibilitas perubahan yang dibuat oleh utas pada variabel bersama.

• Kondisi balapan saat membaca, memeriksa dan menulis variabel bersama.

Kedua masalah ini akan dijelaskan di bagian berikut.



Visibilitas Objek Bersama



Jika dua atau lebih utas berbagi objek tanpa penggunaan yang tepat dari deklarasi volatile atau sinkronisasi, maka perubahan pada objek bersama yang dibuat oleh satu utas mungkin tidak terlihat oleh utas lain.



Bayangkan objek bersama awalnya disimpan di memori utama. Sebuah utas yang berjalan di CPU membaca objek bersama ke dalam cache dari CPU yang sama. Di sana dia membuat perubahan pada objek. Hingga cache CPU telah dikosongkan ke memori utama, versi modifikasi dari objek bersama tidak terlihat oleh utas yang berjalan di CPU lain. Dengan cara ini, setiap utas bisa mendapatkan salinannya sendiri dari objek bersama, setiap salinan akan berada di cache CPU terpisah.



Diagram berikut mengilustrasikan sketsa situasi ini. Satu utas yang berjalan di CPU kiri menyalin objek bersama ke cache dan mengubah nilai variabelcountoleh 2. Perubahan ini tidak terlihat oleh utas lain yang berjalan pada CPU kanan karena pembaruan untuk countbelum dialihkan kembali ke memori utama.



Untuk mengatasi masalah ini, Anda dapat menggunakan volatilesaat mendeklarasikan variabel. Ini dapat memastikan bahwa variabel tertentu dibaca langsung dari memori utama dan selalu ditulis kembali ke memori utama saat diperbarui.



Kondisi balapan



Jika dua atau lebih utas berbagi objek yang sama dan lebih dari satu utas memperbarui variabel dalam objek bersama itu, kondisi balapan mungkin terjadi .



Bayangkan utas A membaca variabel countobjek bersama ke dalam cache prosesornya. Bayangkan juga bahwa utas B melakukan hal yang sama, tetapi dalam cache prosesor yang berbeda. Sekarang utas A menambahkan 1 ke nilai variabel count, dan utas B melakukan hal yang sama. Sekarang var1telah ditingkatkan dua kali - secara terpisah, +1 di cache masing-masing prosesor.



Jika kenaikan ini dilakukan secara berurutan, variabel countakan digandakan dan ditulis kembali ke memori utama + 2.

Namun, dua peningkatan tersebut dilakukan secara bersamaan tanpa sinkronisasi yang tepat. Terlepas dari utas mana (A atau B) yang menulis versi terbarunya countke memori utama, nilai baru hanya akan lebih besar 1 daripada nilai aslinya, meskipun ada dua kenaikan.



Diagram ini menggambarkan terjadinya masalah kondisi balapan yang dijelaskan di atas:



Untuk mengatasi masalah ini, Anda dapat menggunakan blok Java tersinkronisasi... Blok tersinkronisasi memastikan bahwa hanya satu utas yang dapat memasuki bagian kode kritis tertentu pada waktu tertentu. Blok tersinkronisasi juga memastikan bahwa semua variabel yang diakses dalam blok tersinkronisasi dibaca dari memori utama, dan saat utas keluar dari blok tersinkronisasi, semua variabel yang diperbarui akan dikembalikan ke memori utama, terlepas dari apakah variabel tersebut dinyatakan sebagai volatileatau tidak. ...



All Articles