Pengantar Model Memori Java

Model Memori Java (JMM) menjelaskan perilaku utas di lingkungan runtime Java. Model memori adalah bagian dari semantik bahasa Java, dan menjelaskan apa yang dapat dan tidak boleh diharapkan oleh programmer saat mengembangkan perangkat lunak bukan untuk mesin Java tertentu, tetapi untuk Java secara keseluruhan.

Model memori Java asli (yang, khususnya, mengacu pada "memori percolocal"), yang dikembangkan pada tahun 1995, dianggap gagal: banyak pengoptimalan tidak dapat dilakukan tanpa kehilangan jaminan keamanan kode. Secara khusus, ada beberapa opsi untuk menulis "tunggal" multi-utas:

  • baik setiap tindakan mengakses singleton (bahkan ketika objek dibuat lama sekali, dan tidak ada yang bisa berubah) akan menyebabkan kunci antar-thread;
  • atau dalam keadaan tertentu, sistem akan mengeluarkan penyendiri yang belum selesai;
  • atau dalam keadaan tertentu, sistem akan membuat dua penyendiri;
  • atau desain akan bergantung pada perilaku mesin tertentu.

Oleh karena itu, mekanisme memori telah didesain ulang. Pada tahun 2005, dengan dirilisnya Java 5, sebuah pendekatan baru diperkenalkan, yang selanjutnya ditingkatkan dengan dirilisnya Java 14.

Model baru ini didasarkan pada tiga aturan:

Aturan #1 : Program single-threaded berjalan secara berurutan. Artinya: pada kenyataannya, prosesor dapat melakukan beberapa operasi per jam, pada saat yang sama mengubah urutannya, namun, semua ketergantungan data tetap ada, sehingga perilakunya tidak berbeda dari berurutan.

Aturan nomor 2 : tidak ada nilai entah dari mana. Membaca variabel apa pun (kecuali panjang dan ganda yang tidak mudah menguap, yang aturan ini mungkin tidak berlaku) akan mengembalikan nilai default (nol) atau sesuatu yang ditulis di sana oleh perintah lain.

Dan aturan nomor 3 : acara lainnya dieksekusi secara berurutan, jika dihubungkan dengan hubungan pesanan parsial yang ketat "dijalankan sebelum" ( terjadi sebelum ).

Terjadi sebelumnya

Leslie Lamport muncul dengan konsep Happens sebelumnya . Ini adalah hubungan urutan parsial ketat yang diperkenalkan antara perintah atom (++ dan -- bukan atom) dan tidak berarti "secara fisik sebelumnya".

Dikatakan bahwa tim kedua akan "mengetahui" perubahan yang dilakukan oleh tim pertama.

Terjadi sebelumnya

Misalnya, satu dieksekusi sebelum yang lain untuk operasi seperti itu:

Sinkronisasi dan monitor:

  • Menangkap monitor ( metode kunci , mulai tersinkronisasi) dan apa pun yang terjadi di utas yang sama setelahnya.
  • Kembalinya monitor (metode buka kunci , akhir sinkronisasi) dan apa pun yang terjadi di utas yang sama sebelumnya.
  • Mengembalikan monitor dan kemudian menangkapnya dengan utas lain.

Menulis dan membaca:

  • Menulis ke variabel apa pun dan kemudian membacanya di aliran yang sama.
  • Segala sesuatu di utas yang sama sebelum menulis ke variabel volatil, dan tulisan itu sendiri. volatile read dan semua yang ada di utas yang sama setelahnya.
  • Menulis ke variabel yang mudah menguap dan kemudian membacanya lagi. Penulisan yang mudah menguap berinteraksi dengan memori dengan cara yang sama seperti pengembalian monitor, sedangkan pembacaan seperti penangkapan. Ternyata jika satu utas menulis ke variabel yang mudah menguap, dan utas kedua menemukannya, semua yang mendahului penulisan dieksekusi sebelum semua yang muncul setelah pembacaan; Lihat gambar.

Pemeliharaan objek:

  • Inisialisasi statis dan tindakan apa pun dengan instance objek apa pun.
  • Menulis ke bidang final di konstruktor dan semuanya setelah konstruktor. Sebagai pengecualian, relasi happen-before tidak terhubung secara transitif ke aturan lain dan karena itu dapat menyebabkan balapan antar-thread.
  • Pekerjaan apa pun dengan objek dan finalize() .

Layanan streaming:

  • Memulai utas dan kode apa pun di utas.
  • Variabel zeroing yang terkait dengan utas dan kode apa pun di utas.
  • Kode dalam utas dan gabung() ; kode di utas dan isAlive() == false .
  • interrupt() utas dan mendeteksi bahwa itu telah berhenti.

Terjadi sebelum nuansa kerja

Melepaskan monitor yang terjadi sebelum terjadi sebelum memperoleh monitor yang sama. Perlu dicatat bahwa ini adalah rilis, dan bukan pintu keluar, yaitu, Anda tidak perlu khawatir tentang keamanan saat menggunakan tunggu.

Mari kita lihat bagaimana pengetahuan ini akan membantu kita mengoreksi teladan kita. Dalam hal ini, semuanya sangat sederhana: cukup hapus centang eksternal dan biarkan sinkronisasi apa adanya. Sekarang utas kedua dijamin melihat semua perubahan, karena hanya akan mendapatkan monitor setelah utas lainnya melepaskannya. Dan karena dia tidak akan merilisnya sampai semuanya diinisialisasi, kita akan melihat semua perubahan sekaligus, dan tidak secara terpisah:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Menulis ke variabel volatil terjadi-sebelum membaca dari variabel yang sama. Perubahan yang kami buat, tentu saja, memperbaiki bug, tetapi mengembalikan siapa pun yang menulis kode asli ke asalnya - memblokir setiap saat. Kata kunci yang mudah menguap dapat menghemat. Padahal, pernyataan yang dimaksud berarti saat membaca semua yang dinyatakan volatile, kita akan selalu mendapatkan nilai sebenarnya.

Selain itu, seperti yang saya katakan sebelumnya, untuk bidang volatil, penulisan selalu (termasuk panjang dan ganda) merupakan operasi atom. Poin penting lainnya: jika Anda memiliki entitas yang mudah menguap yang memiliki referensi ke entitas lain (misalnya, array, Daftar, atau kelas lain), maka hanya referensi ke entitas itu sendiri yang akan selalu "segar", tetapi tidak ke semua yang ada di dalamnya. itu masuk.

Jadi, kembali ke domba jantan pengunci ganda kami. Menggunakan volatile, Anda dapat memperbaiki situasi seperti ini:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Di sini kita masih memiliki kunci, tetapi hanya jika data == null. Kami memfilter kasus yang tersisa menggunakan pembacaan yang mudah menguap. Ketepatan dipastikan oleh fakta bahwa penyimpanan volatil terjadi-sebelum pembacaan volatil, dan semua operasi yang terjadi di konstruktor dapat dilihat oleh siapa pun yang membaca nilai bidang.