JavaRush /Java Blog /Random-ID /Mengelola volatilitas
lexmirnov
Level 29
Москва

Mengelola volatilitas

Dipublikasikan di grup Random-ID

Pedoman Penggunaan Variabel Volatil

Oleh Brian Goetz 19 Juni 2007 Asli: Mengelola Volatilitas Variabel volatil di Java bisa disebut "sinkronisasi-ringan"; Mereka memerlukan lebih sedikit kode untuk digunakan dibandingkan blok tersinkronisasi, seringkali berjalan lebih cepat, namun hanya dapat melakukan sebagian kecil dari apa yang dilakukan blok tersinkronisasi. Artikel ini menyajikan beberapa pola penggunaan volatil secara efektif—dan beberapa peringatan tentang tempat untuk tidak menggunakannya. Kunci memiliki dua fitur utama: saling pengecualian (mutex) dan visibilitas. Pengecualian bersama berarti bahwa kunci hanya dapat ditahan oleh satu thread pada satu waktu, dan properti ini dapat digunakan untuk mengimplementasikan protokol kontrol akses untuk sumber daya bersama sehingga hanya satu thread yang akan menggunakannya pada satu waktu. Visibilitas adalah masalah yang lebih halus, tujuannya adalah untuk memastikan bahwa perubahan yang dilakukan pada sumber daya publik sebelum kunci dilepaskan akan terlihat oleh thread berikutnya yang mengambil alih kunci tersebut. Jika sinkronisasi tidak menjamin visibilitas, thread dapat menerima nilai variabel publik yang basi atau salah, yang akan menyebabkan sejumlah masalah serius.
Variabel yang mudah menguap
Variabel volatil memiliki sifat visibilitas seperti variabel tersinkronisasi, namun tidak memiliki sifat atomisitas. Artinya, thread akan secara otomatis menggunakan nilai variabel volatil terkini. Mereka dapat digunakan untuk keamanan thread , tetapi dalam serangkaian kasus yang sangat terbatas: kasus yang tidak memperkenalkan hubungan antara beberapa variabel atau antara nilai variabel saat ini dan masa depan. Jadi, volatil saja tidak cukup untuk mengimplementasikan counter, mutex, atau kelas apa pun yang bagiannya yang tidak dapat diubah dikaitkan dengan banyak variabel (misalnya, "mulai <=akhir"). Anda dapat memilih kunci yang mudah menguap karena salah satu dari dua alasan utama: kesederhanaan atau skalabilitas. Beberapa konstruksi bahasa lebih mudah untuk ditulis sebagai kode program, dan kemudian dibaca dan dipahami, ketika mereka menggunakan variabel yang mudah menguap daripada kunci. Selain itu, tidak seperti kunci, kunci tidak dapat memblokir thread sehingga tidak terlalu rentan terhadap masalah skalabilitas. Dalam situasi di mana terdapat lebih banyak pembacaan daripada penulisan, variabel volatil dapat memberikan manfaat kinerja dibandingkan penguncian.
Kondisi untuk penggunaan volatil yang benar
Anda dapat mengganti kunci dengan kunci yang mudah menguap dalam kondisi tertentu. Agar thread aman, kedua kriteria harus dipenuhi:
  1. Apa yang ditulis ke suatu variabel tidak bergantung pada nilainya saat ini.
  2. Variabel tersebut tidak ikut invarian dengan variabel lain.
Sederhananya, kondisi ini berarti bahwa nilai valid yang dapat ditulis ke variabel volatil tidak bergantung pada status program lainnya, termasuk status variabel saat ini. Kondisi pertama mengecualikan penggunaan variabel volatil sebagai penghitung thread-safe. Meskipun kenaikan (x++) terlihat seperti operasi tunggal, sebenarnya ini adalah keseluruhan rangkaian operasi baca-modifikasi-tulis yang harus dilakukan secara atom, yang tidak disediakan oleh volatil. Operasi yang valid mengharuskan nilai x tetap sama sepanjang operasi, yang tidak dapat dicapai dengan menggunakan volatil. (Namun, jika Anda dapat memastikan bahwa nilainya ditulis hanya dari satu thread, kondisi pertama dapat dihilangkan.) Dalam sebagian besar situasi, kondisi pertama atau kedua akan dilanggar, sehingga variabel volatil menjadi pendekatan yang kurang umum digunakan untuk mencapai keamanan thread dibandingkan variabel tersinkronisasi. Listing 1 menunjukkan kelas non-thread-safe dengan rentang angka. Ini berisi invarian - batas bawah selalu lebih kecil atau sama dengan batas atas. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Karena variabel status rentang dibatasi dengan cara ini, membuat bidang bawah dan atas mudah berubah tidak akan cukup untuk memastikan kelas aman untuk thread; sinkronisasi masih diperlukan. Jika tidak, cepat atau lambat Anda akan kurang beruntung dan dua thread yang menjalankan setLower() dan setUpper() dengan nilai yang tidak sesuai dapat menyebabkan rentang ke keadaan tidak konsisten. Misalnya, jika nilai awalnya adalah (0, 5), thread A memanggil setLower(4), dan pada saat yang sama thread B memanggil setUpper(3), operasi yang disisipkan ini akan menghasilkan kesalahan, meskipun keduanya akan lolos pemeriksaan yang seharusnya melindungi invarian. Akibatnya, rentangnya menjadi (4, 3) - nilai yang salah. Kita perlu membuat setLower() dan setUpper() bersifat atomik ke operasi rentang lainnya - dan membuat bidang mudah berubah tidak akan berhasil.
Pertimbangan Kinerja
Alasan pertama menggunakan volatil adalah kesederhanaan. Dalam beberapa situasi, menggunakan variabel seperti itu lebih mudah daripada menggunakan kunci yang terkait dengannya. Alasan kedua adalah performa, terkadang volatil akan bekerja lebih cepat dibandingkan lock. Sangat sulit untuk membuat pernyataan yang tepat dan mencakup semua seperti "X selalu lebih cepat dari Y," terutama jika menyangkut operasi internal Java Virtual Machine. (Misalnya, JVM mungkin melepaskan kunci seluruhnya dalam beberapa situasi, sehingga sulit untuk mendiskusikan biaya volatil versus sinkronisasi secara abstrak). Namun, pada sebagian besar arsitektur prosesor modern, biaya pembacaan variabel volatil tidak jauh berbeda dengan biaya pembacaan variabel biasa. Biaya penulisan volatil jauh lebih tinggi dibandingkan penulisan variabel biasa karena diperlukan pagar memori untuk visibilitas, namun umumnya lebih murah dibandingkan pengaturan kunci.
Pola penggunaan volatil yang tepat
Banyak pakar konkurensi cenderung menghindari penggunaan variabel volatil karena lebih sulit digunakan dengan benar dibandingkan kunci. Namun, ada beberapa pola yang terdefinisi dengan baik, yang jika diikuti dengan hati-hati, dapat digunakan dengan aman dalam berbagai situasi. Selalu hormati batasan-batasan dari volatil - hanya gunakan volatil yang tidak bergantung pada hal lain dalam program, dan ini akan mencegah Anda masuk ke wilayah berbahaya dengan pola-pola ini.
Pola #1: Bendera Status
Mungkin penggunaan kanonik dari variabel yang dapat diubah adalah tanda status boolean sederhana yang menunjukkan bahwa peristiwa siklus hidup penting yang terjadi satu kali saja telah terjadi, seperti penyelesaian inisialisasi atau permintaan penghentian. Banyak aplikasi menyertakan konstruksi kontrol dalam bentuk: "sampai kita siap untuk mematikan, terus berjalan" seperti yang ditunjukkan pada Listing 2: Kemungkinan volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } metode shutdown() akan dipanggil dari suatu tempat di luar loop - di thread lain - jadi sinkronisasi diperlukan untuk memastikan penutupan visibilitas variabel yang benarDiminta. (Ini dapat dipanggil dari pendengar JMX, pendengar tindakan di thread acara GUI, melalui RMI, melalui layanan web, dll.). Namun, perulangan dengan blok tersinkronisasi akan jauh lebih rumit daripada perulangan dengan tanda status volatil seperti pada Listing 2. Karena volatil membuat penulisan kode lebih mudah dan tanda status tidak bergantung pada status program lainnya, ini adalah contoh dari a penggunaan yang baik dari volatil. Ciri khas dari bendera status tersebut adalah biasanya hanya terdapat satu transisi keadaan; flag shutdownRequested berubah dari false menjadi true, dan kemudian program dimatikan. Pola ini dapat diperluas ke bendera negara bagian yang dapat berubah-ubah, namun hanya jika siklus transisi (dari salah ke benar ke salah) terjadi tanpa intervensi pihak luar. Jika tidak, diperlukan semacam mekanisme transisi atom, seperti variabel atom.
Pola #2: Penerbitan aman satu kali
Kesalahan visibilitas yang mungkin terjadi ketika tidak ada sinkronisasi bisa menjadi masalah yang lebih sulit ketika menulis referensi objek dan bukan nilai primitif. Tanpa sinkronisasi, Anda dapat melihat nilai saat ini untuk referensi objek yang ditulis oleh thread lain dan masih melihat nilai status basi untuk objek tersebut. (Ancaman ini adalah akar masalah kunci pemeriksaan ulang yang terkenal, di mana referensi objek dibaca tanpa sinkronisasi, dan Anda berisiko melihat referensi sebenarnya tetapi mendapatkan objek yang dibuat sebagian melalui referensi tersebut.) Salah satu cara untuk memublikasikan referensi dengan aman objek adalah untuk membuat referensi ke objek yang mudah menguap. Listing 3 menunjukkan contoh ketika, saat startup, thread latar belakang memuat beberapa data dari database. Kode lain yang mungkin mencoba menggunakan data ini memeriksa apakah telah dipublikasikan sebelum mencoba menggunakannya. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Jika referensi ke theFlooble tidak mudah berubah, kode di doWork() akan berisiko melihat Flooble yang dibuat sebagian saat mencoba mereferensikan theFlooble. Persyaratan utama untuk pola ini adalah bahwa objek yang dipublikasikan harus aman untuk thread atau tidak dapat diubah secara efektif (tidak dapat diubah secara efektif berarti statusnya tidak pernah berubah setelah dipublikasikan). Tautan Volatile dapat memastikan bahwa suatu objek terlihat dalam bentuk yang dipublikasikan, namun jika status objek berubah setelah dipublikasikan, diperlukan sinkronisasi tambahan.
Pola #3: Observasi Independen
Contoh sederhana lainnya dari penggunaan volatil yang aman adalah ketika pengamatan “dipublikasikan” secara berkala untuk digunakan dalam suatu program. Misalnya saja ada sensor lingkungan yang mendeteksi suhu saat ini. Thread latar belakang dapat membaca sensor ini setiap beberapa detik dan memperbarui variabel volatil yang berisi suhu saat ini. Thread lain kemudian dapat membaca variabel ini, mengetahui bahwa nilai di dalamnya selalu terbaru. Kegunaan lain dari pola ini adalah mengumpulkan statistik tentang program. Listing 4 menunjukkan bagaimana mekanisme otentikasi dapat mengingat nama pengguna yang terakhir login. Referensi lastUser akan digunakan kembali untuk memposting nilai untuk digunakan oleh program lainnya. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Pola ini memperluas pola sebelumnya; nilainya dipublikasikan untuk digunakan di tempat lain dalam program, tetapi publikasi tersebut bukanlah peristiwa satu kali, melainkan serangkaian peristiwa independen. Pola ini mengharuskan nilai yang dipublikasikan tidak dapat diubah secara efektif - sehingga statusnya tidak berubah setelah dipublikasikan. Kode yang menggunakan nilai harus menyadari bahwa itu dapat berubah sewaktu-waktu.
Pola #4: pola “kacang volatil”.
Pola "volatile bean" berlaku dalam kerangka kerja yang menggunakan JavaBeans sebagai "struktur yang dimuliakan". Pola “volatile bean” menggunakan JavaBean sebagai wadah untuk sekelompok properti independen dengan pengambil dan/atau penyetel. Alasan untuk pola "volatile bean" adalah bahwa banyak kerangka kerja menyediakan wadah untuk pemegang data yang dapat diubah (seperti HttpSession), namun objek yang ditempatkan dalam wadah ini harus aman untuk thread. Dalam pola kacang volatil, semua elemen data JavaBean bersifat volatil, dan pengambil serta penyetel harus sepele - elemen tersebut tidak boleh berisi logika apa pun selain mendapatkan atau menyetel properti yang sesuai. Selain itu, untuk anggota data yang merupakan referensi objek, objek tersebut harus tidak dapat diubah secara efektif. (Ini melarang kolom referensi array, karena ketika referensi array dinyatakan volatil, hanya referensi tersebut, dan bukan elemen itu sendiri, yang memiliki properti volatil.) Seperti halnya variabel volatil lainnya, tidak ada invarian atau batasan yang terkait dengan properti JavaBeans . Contoh JavaBean yang ditulis menggunakan pola “volatile bean” ditunjukkan pada Listing 5: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Pola volatil yang lebih kompleks
Pola di bagian sebelumnya mencakup sebagian besar kasus umum di mana penggunaan volatil adalah hal yang masuk akal dan jelas. Bagian ini membahas pola yang lebih kompleks di mana volatil dapat memberikan manfaat kinerja atau skalabilitas. Pola volatil yang lebih maju bisa menjadi sangat rapuh. Asumsi Anda harus didokumentasikan dengan cermat dan pola-pola ini dikemas dengan kuat, karena perubahan sekecil apa pun dapat merusak kode Anda! Selain itu, mengingat alasan utama kasus penggunaan volatil yang lebih kompleks adalah performa, pastikan Anda benar-benar memiliki kebutuhan yang jelas akan peningkatan performa yang diinginkan sebelum menggunakannya. Pola-pola ini merupakan kompromi yang mengorbankan keterbacaan atau kemudahan pemeliharaan untuk kemungkinan peningkatan kinerja - jika Anda tidak memerlukan peningkatan kinerja (atau tidak dapat membuktikan bahwa Anda memerlukannya dengan program pengukuran yang ketat), maka itu mungkin merupakan kesepakatan yang buruk karena itu Anda melepaskan sesuatu yang berharga dan mendapatkan sesuatu yang lebih sedikit sebagai imbalannya.
Pola #5: Kunci baca-tulis yang murah
Sekarang Anda harus menyadari bahwa volatil terlalu lemah untuk menerapkan counter. Karena ++x pada dasarnya adalah pengurangan dari tiga operasi (baca, tambahkan, simpan), jika terjadi kesalahan, Anda akan kehilangan nilai yang diperbarui jika beberapa thread mencoba menambah penghitung volatil secara bersamaan. Namun, jika terdapat lebih banyak pembacaan daripada perubahan, Anda dapat menggabungkan penguncian intrinsik dan variabel volatil untuk mengurangi overhead jalur kode secara keseluruhan. Listing 6 menunjukkan penghitung thread-safe yang menggunakan sinkronisasi untuk memastikan bahwa operasi kenaikan bersifat atomik, dan menggunakan volatil untuk memastikan bahwa hasil saat ini terlihat. Jika pembaruan jarang dilakukan, pendekatan ini dapat meningkatkan kinerja karena biaya baca terbatas pada pembacaan yang tidak stabil, yang umumnya lebih murah daripada memperoleh kunci yang tidak bertentangan. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } Alasan mengapa metode ini disebut "kunci baca-tulis murah" adalah karena Anda menggunakan mekanisme pengaturan waktu yang berbeda untuk membaca dan menulis. Karena operasi tulis dalam kasus ini melanggar ketentuan pertama penggunaan volatil, Anda tidak dapat menggunakan volatil untuk mengimplementasikan penghitung dengan aman - Anda harus menggunakan kunci. Namun, Anda dapat menggunakan volatil untuk membuat nilai saat ini terlihat saat membaca, sehingga Anda menggunakan kunci untuk semua operasi modifikasi dan volatil untuk operasi baca-saja. Jika kunci hanya mengizinkan satu thread pada satu waktu untuk mengakses suatu nilai, pembacaan yang mudah menguap mengizinkan lebih dari satu, jadi ketika Anda menggunakan volatil untuk melindungi pembacaan, Anda mendapatkan tingkat pertukaran yang lebih tinggi dibandingkan jika Anda menggunakan kunci pada semua kode: dan membaca, dan mencatat. Namun, waspadai kerapuhan pola ini: dengan dua mekanisme sinkronisasi yang saling bersaing, hal ini bisa menjadi sangat rumit jika Anda melampaui penerapan paling dasar dari pola ini.
Ringkasan
Variabel volatil adalah bentuk sinkronisasi yang lebih sederhana namun lebih lemah daripada penguncian, yang dalam beberapa kasus memberikan kinerja atau skalabilitas yang lebih baik daripada penguncian intrinsik. Jika Anda memenuhi ketentuan untuk penggunaan volatil yang aman - suatu variabel benar-benar independen terhadap variabel lain dan nilai sebelumnya - Anda terkadang dapat menyederhanakan kode dengan mengganti yang disinkronkan dengan volatil. Namun, kode yang menggunakan volatil seringkali lebih rapuh dibandingkan kode yang menggunakan penguncian. Pola yang disarankan di sini mencakup kasus-kasus paling umum di mana volatilitas merupakan alternatif yang masuk akal untuk sinkronisasi. Dengan mengikuti pola-pola ini - dan berhati-hati untuk tidak mendorongnya melampaui batasnya - Anda dapat menggunakan volatil dengan aman jika pola tersebut memberikan manfaat.
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION