Panduan untuk Kata Kunci Volatile di Java

1. Gambaran keseluruhan

Sekiranya tidak ada penyegerakan yang diperlukan, penyusun, waktu proses, atau pemproses dapat menerapkan segala macam pengoptimuman. Walaupun pengoptimuman ini bermanfaat sepanjang masa, kadang-kadang ia boleh menyebabkan masalah halus.

Cache dan penyusunan semula adalah antara pengoptimuman yang mungkin mengejutkan kita dalam konteks serentak. Java dan JVM menyediakan banyak cara untuk mengawal susunan memori, dan kata kunci yang tidak stabil adalah salah satunya.

Dalam artikel ini, kita akan memfokuskan konsep asas tetapi sering disalahpahami dalam bahasa Jawa - kata kunci yang tidak menentu . Pertama, kita akan mulakan dengan sedikit latar belakang mengenai cara kerja seni bina komputer yang mendasari, dan kemudian kita akan mengetahui susunan memori di Java.

2. Senibina Multiprosesor Berkongsi

Pemproses bertanggungjawab untuk melaksanakan arahan program. Oleh itu, mereka perlu mengambil kedua arahan program dan data yang diperlukan dari RAM.

Oleh kerana CPU mampu menjalankan sebilangan besar arahan per saat, pengambilan dari RAM bukanlah sesuatu yang sesuai untuk mereka. Untuk memperbaiki keadaan ini, pemproses menggunakan trik seperti Eksekusi Tidak Tertib, Ramalan Cabang, Pelaksanaan Spekulatif, dan, tentu saja, Caching.

Di sinilah hierarki memori berikut dimainkan:

Oleh kerana inti yang berbeza melaksanakan lebih banyak arahan dan memanipulasi lebih banyak data, mereka mengisi cache mereka dengan data dan petunjuk yang lebih relevan. Ini akan meningkatkan prestasi keseluruhan dengan mengorbankan cabaran koherensi cache .

Ringkasnya, kita harus berfikir dua kali tentang apa yang berlaku apabila satu utas mengemas kini nilai cache.

3. Bilakah Menggunakan Volatile

Untuk memperluas lebih jauh mengenai koherensi cache, mari pinjam satu contoh dari buku Java Concurrency in Practice:

public class TaskRunner { private static int number; private static boolean ready; private static class Reader extends Thread { @Override public void run() { while (!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new Reader().start(); number = 42; ready = true; } }

The TaskRunner kelas mengekalkan dua pembolehubah mudah. Dalam kaedah utamanya, ia membuat utas lain yang berputar pada pemboleh ubah siap selagi ia salah. Apabila pemboleh ubah menjadi benar, utas hanya akan mencetak pemboleh ubah nombor .

Banyak yang mungkin mengharapkan program ini hanya mencetak 42 setelah penundaan yang singkat. Namun, pada hakikatnya, kelewatan mungkin lebih lama. Ia mungkin bertahan selama-lamanya, atau mencetak sifar!

Penyebab anomali ini adalah kekurangan penglihatan dan penyusunan semula memori yang betul . Mari menilai mereka dengan lebih terperinci.

3.1. Penglihatan Ingatan

Dalam contoh ringkas ini, kami mempunyai dua utas aplikasi: utas utama dan utas pembaca. Mari bayangkan senario di mana OS menjadualkan utas pada dua teras CPU yang berbeza, di mana:

  • Utas utama mempunyai salinan pemboleh ubah siap dan nombor dalam cache terasnya
  • Benang pembaca berakhir dengan salinannya juga
  • Benang utama mengemas kini nilai cache

Pada kebanyakan pemproses moden, permintaan tulis tidak akan langsung diterapkan setelah dikeluarkan. Sebenarnya, pemproses cenderung mengantri penulisan tersebut dalam penyangga tulis khas . Selepas beberapa ketika, mereka akan menerapkan penulisan tersebut ke memori utama sekaligus.

Dengan semua yang dikatakan, apabila utas utama mengemas kini nombor dan pemboleh ubah siap , tidak ada jaminan tentang apa yang dilihat oleh utas pembaca. Dengan kata lain, utas pembaca dapat melihat nilai yang dikemas kini dengan segera, atau dengan sedikit kelewatan, atau tidak pernah sama sekali!

Keterlihatan memori ini boleh menyebabkan masalah hidup dalam program yang bergantung pada keterlihatan.

3.2. Menyusun semula

Untuk memburukkan lagi keadaan, utas pembaca dapat melihat penulisan tersebut mengikut urutan selain daripada susunan program yang sebenarnya . Contohnya, sejak pertama kali kita mengemas kini pemboleh ubah nombor :

public static void main(String[] args) { new Reader().start(); number = 42; ready = true; }

Kami mungkin menjangkakan cetakan benang pembaca 42. Walau bagaimanapun, sebenarnya dapat melihat sifar sebagai nilai cetak!

Penyusunan semula adalah teknik pengoptimuman untuk peningkatan prestasi. Menariknya, komponen yang berbeza mungkin menggunakan pengoptimuman ini:

  • Pemproses boleh melenyapkan buffer tulisnya dalam urutan apa pun selain pesanan program
  • Pemproses mungkin menggunakan teknik pelaksanaan di luar pesanan
  • Penyusun JIT dapat mengoptimumkan melalui penyusunan semula

3.3. Perintah Memori yang tidak menentu

Untuk memastikan bahawa kemas kini ke pemboleh ubah disebarkan dengan ramalan ke utas lain, kita harus menerapkan pengubah yang tidak stabil pada pemboleh ubah tersebut:

public class TaskRunner { private volatile static int number; private volatile static boolean ready; // same as before }

Dengan cara ini, kami berkomunikasi dengan runtime dan pemproses untuk tidak menyusun semula arahan yang melibatkan pemboleh ubah tidak menentu . Juga, pemproses memahami bahawa mereka harus segera mengemas kini sebarang pemboleh ubah pada pemboleh ubah ini.

4. tidak menentu dan Thread Penyegerakan

Untuk aplikasi multithread, kita perlu memastikan beberapa peraturan untuk tingkah laku yang konsisten:

  • Saling Pengecualian - hanya satu utas yang melaksanakan bahagian kritikal dalam satu masa
  • Keterlihatan - perubahan yang dibuat oleh satu utas pada data yang dikongsi dapat dilihat oleh utas lain untuk mengekalkan konsistensi data

kaedah dan blok yang disegerakkan memberikan kedua-dua sifat di atas, dengan kos prestasi aplikasi.

tidak menentu adalah kata kunci yang sangat berguna kerana dapat membantu memastikan aspek penglihatan data berubah tanpa, tentu saja, memberikan pengecualian bersama . Oleh itu, ia berguna di tempat-tempat di mana kita baik-baik saja dengan banyak utas yang menjalankan sekatan kod secara selari, tetapi kita perlu memastikan sifat keterlihatan.

5. Berlaku-Sebelum Memesan

The memory visibility effects of volatile variables extend beyond the volatile variables themselves.

To make matters more concrete, let's suppose thread A writes to a volatile variable, and then thread B reads the same volatile variable. In such cases, the values that were visible to A before writing the volatile variable will be visible to B after reading the volatile variable:

Technically speaking, any write to a volatile field happens before every subsequent read of the same field. This is the volatile variable rule of the Java Memory Model (JMM).

5.1. Piggybacking

Because of the strength of the happens-before memory ordering, sometimes we can piggyback on the visibility properties of another volatile variable. For instance, in our particular example, we just need to mark the ready variable as volatile:

public class TaskRunner { private static int number; // not volatile private volatile static boolean ready; // same as before }

Anything prior to writing true to the ready variable is visible to anything after reading the ready variable. Therefore, the number variable piggybacks on the memory visibility enforced by the ready variable. Put simply, even though it's not a volatile variable, it is exhibiting a volatile behavior.

Dengan menggunakan semantik ini, kita hanya dapat menentukan beberapa pemboleh ubah dalam kelas kita sebagai tidak stabil dan mengoptimumkan jaminan penglihatan.

6. Kesimpulannya

Dalam tutorial ini, kami telah meneroka lebih banyak mengenai kata kunci yang tidak menentu dan kemampuannya, serta penambahbaikan yang dibuat bermula dengan Java 5.

Seperti biasa, contoh kod boleh didapati di GitHub.