Untuk apa pagar memori digunakan di Jawa?

18

Ketika mencoba memahami bagaimana SubmissionPublisher( kode sumber di Java SE 10, OpenJDK | docs ), sebuah kelas baru yang ditambahkan ke Java SE dalam versi 9, telah diterapkan, saya menemukan beberapa panggilan API yang VarHandlesebelumnya tidak saya sadari:

fullFence, acquireFence, releaseFence, loadLoadFenceDan storeStoreFence.

Setelah melakukan beberapa penelitian, terutama mengenai konsep hambatan / pagar ingatan (saya pernah mendengar tentang mereka sebelumnya, ya; tetapi tidak pernah menggunakannya, sehingga cukup tidak terbiasa dengan semantik mereka), saya pikir saya memiliki pemahaman dasar tentang apa itu untuk . Meskipun demikian, karena pertanyaan saya mungkin timbul dari kesalahpahaman, saya ingin memastikan bahwa saya telah melakukannya dengan benar:

  1. Hambatan ingatan adalah kendala menata ulang tentang operasi membaca dan menulis.

  2. Hambatan memori dapat dikategorikan ke dalam dua kategori utama: hambatan memori searah dan dua arah, tergantung pada apakah mereka menetapkan batasan baik membaca atau menulis atau keduanya.

  3. C ++ mendukung berbagai hambatan memori , namun, ini tidak cocok dengan yang disediakan oleh VarHandle. Namun, beberapa hambatan memori yang tersedia dalam VarHandlememberikan efek pemesanan yang kompatibel dengan hambatan memori C ++ yang sesuai.

    • #fullFence kompatibel untuk atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence kompatibel untuk atomic_thread_fence(memory_order_acquire)
    • #releaseFence kompatibel untuk atomic_thread_fence(memory_order_release)
    • #loadLoadFencedan #storeStoreFencetidak memiliki bagian penghitung C ++ yang kompatibel

Kata yang kompatibel tampaknya sangat penting di sini karena semantiknya jelas berbeda dalam hal perincian. Sebagai contoh, semua hambatan C ++ adalah dua arah, sedangkan hambatan Java tidak (harus).

  1. Sebagian besar hambatan memori juga memiliki efek sinkronisasi. Itu terutama tergantung pada jenis penghalang yang digunakan dan instruksi penghalang yang sebelumnya dieksekusi di utas lainnya. Karena implikasi penuh dari instruksi penghalang adalah spesifik untuk perangkat keras, saya akan tetap menggunakan penghalang level lebih tinggi (C ++). Dalam C ++, misalnya, perubahan yang dibuat sebelum instruksi barrier rilis terlihat oleh thread yang menjalankan instruksi barrier memperoleh .

Apakah asumsi saya benar? Jika demikian, pertanyaan saya yang muncul adalah:

  1. Apakah hambatan memori tersedia dalam VarHandlepenyebab sinkronisasi jenis apa pun?

  2. Terlepas dari apakah mereka menyebabkan sinkronisasi memori atau tidak, kendala pemesanan ulang apa yang berguna untuk Java? Java Memory Model sudah memberikan jaminan yang sangat kuat terkait pemesanan ketika bidang yang mudah berubah, kunci atau VarHandleoperasi seperti #compareAndSetterlibat.

Jika Anda mencari contoh: Yang disebutkan di atas BufferedSubscription, kelas dalam dari SubmissionPublisher(sumber terkait di atas), membuat pagar penuh di baris 1079 (berfungsi growAndAdd; karena situs web yang ditautkan tidak mendukung pengidentifikasi fragmen, hanya CTRL + F untuk itu ). Namun, tidak jelas bagi saya untuk apa itu.

Quaffel
sumber
1
Saya sudah mencoba menjawab, tetapi untuk membuatnya sangat sederhana, mereka ada karena orang menginginkan mode yang lebih lemah daripada yang dimiliki Java. Dalam urutan menaik, ini akan menjadi: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Jawaban:

11

Ini terutama bukan jawaban, sungguh (awalnya ingin memberikan komentar, tetapi seperti yang Anda lihat, ini terlalu lama). Hanya saja saya sering mempertanyakan ini sendiri, melakukan banyak membaca dan meneliti dan pada saat ini saya dapat dengan aman mengatakan: ini rumit. Saya bahkan menulis beberapa tes dengan jcstress untuk mengetahui bagaimana sebenarnya mereka bekerja (sambil melihat kode assembly yang dihasilkan) dan sementara beberapa dari mereka entah bagaimana masuk akal, subjek secara umum tidak berarti mudah.

Hal pertama yang perlu Anda pahami:

Spesifikasi Bahasa Jawa (JLS) tidak menyebutkan hambatan , di mana pun. Ini, untuk java, akan menjadi detail implementasi: itu benar-benar bertindak dalam hal terjadi sebelum semantik. Untuk dapat menentukan ini sesuai dengan JMM (Java Memory Model), JMM harus banyak berubah .

Ini sedang dalam proses.

Kedua, jika Anda benar-benar ingin menggaruk permukaan di sini, ini adalah hal pertama yang harus diperhatikan . Pembicaraan itu luar biasa. Bagian favorit saya adalah ketika Herb Sutter mengangkat 5 jarinya dan berkata, "Ini adalah berapa banyak orang yang dapat bekerja dengan benar dan benar dengan ini." Itu akan memberi Anda sedikit kerumitan yang terlibat. Namun demikian, ada beberapa contoh sepele yang mudah dipahami (seperti penghitung yang diperbarui oleh banyak utas yang tidak peduli dengan jaminan memori lainnya , tetapi hanya peduli bahwa itu sendiri akan bertambah dengan benar).

Contoh lain adalah ketika (dalam java) Anda ingin volatilebendera untuk mengontrol utas untuk berhenti / mulai. Anda tahu, klasik:

volatile boolean stop = false; // on thread writes, one thread reads this    

Jika Anda bekerja dengan java, Anda akan tahu bahwa tanpa volatile kode ini rusak (Anda dapat membaca mengapa penguncian cek ganda rusak tanpa itu misalnya). Tetapi apakah Anda juga tahu bahwa bagi sebagian orang yang menulis kode kinerja tinggi ini terlalu banyak? volatilebaca / tulis juga menjamin konsistensi berurutan - yang memiliki beberapa jaminan kuat dan beberapa orang menginginkan versi yang lebih lemah ini.

Bendera aman utas, tetapi tidak mudah menguap? Ya, tepatnya: VarHandle::set/getOpaque.

Dan Anda akan mempertanyakan mengapa seseorang mungkin membutuhkannya misalnya? Tidak semua orang tertarik dengan semua perubahan yang didukung oleh babi volatile.

Mari kita lihat bagaimana kita mencapai ini di java. Pertama-tama, seperti eksotis hal yang sudah ada di API: AtomicInteger::lazySet. Ini tidak ditentukan dalam Java Memory Model dan tidak memiliki definisi yang jelas ; masih orang menggunakannya (LMAX, afaik atau ini untuk membaca lebih lanjut ). IMHO, AtomicInteger::lazySetadalah VarHandle::releaseFence(atau VarHandle::storeStoreFence).


Mari kita coba jawab mengapa seseorang membutuhkan ini ?

JMM pada dasarnya memiliki dua cara untuk mengakses bidang: polos dan volatil (yang menjamin konsistensi berurutan ). Semua metode ini yang Anda sebutkan ada di sana untuk membawa sesuatu di antara keduanya - rilis / dapatkan semantik ; ada kasus, saya kira, di mana orang benar - benar membutuhkan ini.

Bahkan lebih relaksasi dari rilis / memperoleh akan buram , yang saya masih mencoba untuk memahami .


Jadi intinya (pemahaman Anda cukup benar, btw): jika Anda berencana untuk menggunakan ini di java - mereka tidak memiliki spesifikasi saat ini, lakukan dengan risiko Anda sendiri. Jika Anda ingin memahaminya, mode yang setara dengan C ++ adalah tempat untuk memulai.

Eugene
sumber
1
Jangan mencoba mencari tahu makna lazySetdengan menghubungkan ke jawaban kuno, dokumentasi saat ini tepatnya mengatakan apa artinya, saat ini. Lebih lanjut, itu menyesatkan untuk mengatakan bahwa JMM hanya memiliki dua mode akses. Kami memiliki read dan volatile write yang volatile , yang bersama-sama dapat membangun hubungan yang terjadi sebelum .
Holger
1
Saya sedang menulis sesuatu tentangnya. Pertimbangkan bahwa cas adalah keduanya, baca dan tulis, bertindak seperti penghalang penuh, dan Anda mungkin mengerti, mengapa santai itu diinginkan. Misalnya ketika menerapkan kunci, tindakan pertama adalah cas (0, 1) pada jumlah kunci, tetapi Anda hanya perlu memperoleh semantik (seperti volatile read), sedangkan penulisan akhir 0 untuk membuka kunci harus memiliki rilis semantik (seperti volatile write ), jadi ada yang terjadi sebelum antara membuka dan mengunci berikutnya. Acquire / Release bahkan lebih lemah dari Volatile Read / Write tentang utas menggunakan kunci yang berbeda.
Holger
1
@Peter Cordes: Versi C pertama yang memiliki volatilekata kunci adalah C99, lima tahun setelah Java, tetapi masih tidak memiliki semantik yang berguna, bahkan C ++ 03 tidak memiliki Model Memori. Hal-hal yang oleh C ++ disebut "atomik" juga jauh lebih muda dari Jawa. Dan volatilekata kunci bahkan tidak menyiratkan pembaruan atom. Jadi mengapa harus dinamai demikian.
Holger
1
@PeterCordes mungkin, saya bingung dengan restrict, namun, saya ingat saat-saat ketika saya harus menulis __volatileuntuk menggunakan ekstensi kompiler non-kata kunci. Jadi mungkin, itu tidak mengimplementasikan C89 sepenuhnya? Jangan bilang saya yang lama. Sebelum Java 5, volatilejauh lebih dekat dengan C. Tetapi Java tidak memiliki MMIO, jadi tujuannya selalu multi-threading, tetapi semantik pra-Java 5 tidak terlalu berguna untuk itu. Jadi rilis / peroleh seperti semantik ditambahkan, tapi tetap saja itu bukan atomik (pembaruan atom adalah fitur tambahan yang dibangun di atasnya).
Holger
2
@Eugene mengenai hal ini , contoh saya khusus untuk menggunakan cas untuk mengunci yang akan diperoleh. Kait hitung mundur akan menanggung penurunan atom dengan rilis semantik, diikuti oleh utas yang mencapai nol memasukkan pagar peroleh dan menjalankan aksi terakhir. Tentu saja, ada kasus lain untuk pembaruan atom di mana pagar penuh tetap diperlukan.
Holger