JavaBeans: sifat, acara, dan keselamatan utas

Java adalah bahasa dinamik yang merangkumi konstruk bahasa dan kelas sokongan multithreading yang mudah digunakan. Banyak program Java bergantung pada multithreading untuk memanfaatkan paralelisme aplikasi dalaman, meningkatkan prestasi rangkaian, atau mempercepat tindak balas maklum balas pengguna. Sebilangan besar jangka masa Java menggunakan multithreading untuk melaksanakan fitur pengumpulan sampah Java. Akhirnya, AWT juga bergantung pada utas yang berasingan untuk berfungsi. Ringkasnya, bahkan program Java yang paling sederhana dilahirkan dalam persekitaran multithreading yang aktif.

Oleh itu, kacang Java juga ditempatkan di lingkungan multithread yang dinamis, dan di sinilah bahaya klasik menghadapi keadaan perlumbaan . Keadaan perlumbaan adalah senario aliran program yang bergantung pada masa yang boleh menyebabkan rasuah keadaan (data program). Pada bahagian berikut saya akan memperincikan dua senario tersebut. Setiap biji Java perlu dirancang dengan mempertimbangkan kondisi ras agar kacang dapat menahan penggunaan serentak oleh beberapa utas klien.

Masalah multithreading dengan sifat sederhana

Pelaksanaan kacang mesti menganggap bahawa banyak utas mengakses dan / atau mengubah contoh kacang tunggal pada masa yang sama. Sebagai contoh kacang yang dilaksanakan dengan tidak betul (kerana berkaitan dengan kesadaran multithreading), pertimbangkan kacang BrokenProperties berikut dan program ujian MTProperties yang berkaitan:

BrokenProperties.java

import java.awt.Point;

// Demo Bean yang tidak melindungi penggunaan pelbagai benang.

kelas awam BrokenProperties memperluas Titik {

// ------------------------------------------------ ------------------- // set () / get () untuk harta 'Spot' // --------------- -------------------------------------------------- -

public void setSpot (Titik titik) {// 'spot' setter this.x = point.x; ini.y = point.y;

} public Point getSpot () {// 'spot' getter mengembalikan ini; }} // Akhir Kekacang Kacang / Kelas Rusak

MTProperties.java

import java.awt.Point; utiliti import. *; import utiliti. kacang. *;

MTProperties kelas awam meluaskan Thread {

dilindungi BrokenProperties myBean; // kacang sasaran untuk mengalahkan ..

dilindungi int myID; // setiap utas membawa sedikit ID

// ------------------------------------------------ ------------------- // utama () titik masuk // ---------------------- --------------------------------------------- awam kosong statik utama ( Rentetan [] argumen) {

Kacang BrokenProperties; Benang benang;

bean = (BrokenProperties) BeansKit.newBean ("BrokenProperties");

untuk (int i = 0; i <20; i ++) {// mulakan 20 utas ke benang kacang bash = MTProperties baru (kacang, i); // utas mendapat akses ke bean thread.start (); }} // ---------------------------------------------- --------------------- // Pembina MTProperties // ----------------------- --------------------------------------------

awam MTProperties (BrokenProperties bean, int id) {this.myBean = kacang; // perhatikan kacang untuk mengatasi ini.myID = id; // perhatikan siapa kita} // ----------------------------------------- -------------------------- // gelung utama utas: // buat selamanya // buat Titik rawak baru dengan x == y // beritahu kacang untuk mengadopsi Point sebagai harta 'spot' barunya // tanya kacang apa harta 'spot' yang sekarang ditetapkan untuk // membuang goyangan jika spot x tidak sama dengan spot y // --------- -------------------------------------------------- -------- larian kekosongan awam () {int someInt; Titik titik = Titik baru ();

sementara (benar) {someInt = (int) (Math.random () * 100); point.x = someInt; point.y = someInt; myBean.setSpot (titik);

point = myBean.getSpot (); if (point.x! = point.y) {System.out.println ("Bean rosak! x =" + point.x + ", y =" + point.y); System.exit (10); } System.out.print ((char) ('A' + myID)); System.out.flush (); }}} // MTProperties Akhir Kelas

Catatan: Pakej utiliti yang diimport MTPropertiesmengandungi kelas yang boleh digunakan semula dan kaedah statik yang dikembangkan untuk buku oleh pengarang.

Kedua senarai kod sumber di atas menentukan kacang yang disebut BrokenProperties dan kelas MTProperties, yang digunakan untuk menjalankan kacang dari dalam 20 utas berjalan. Mari kita ikuti MTProperties' main()entry point ' : Pertama ia mewujudkan kacang BrokenProperties, diikuti dengan penciptaan dan permulaan 20 utas. Kelas MTPropertiesmemanjangkan java.lang.Thread, jadi semua yang perlu kita lakukan untuk menjadikan kelas MTPropertieske dalam thread adalah untuk kelas langkau Thread's run()kaedah. Pembina untuk utas kami mengambil dua argumen: objek kacang yang akan berkomunikasi dengan benang dan pengenalan yang unik, yang membolehkan 20 utas dibezakan dengan mudah pada waktu berjalan.

Tamat perniagaan demo ini adalah run()kaedah kami di kelas MTProperties. Di sini kita berpusing selamanya, mencipta titik baru (x, y) secara rawak, tetapi dengan ciri berikut: koordinat x mereka selalu sama dengan koordinat y mereka. Titik-titik rawak ini diteruskan ke setSpot()kaedah setter kacang dan kemudian segera dibaca semula menggunakan getSpot()kaedah getter. Anda menjangkakan spotharta yang dibaca sama dengan titik rawak yang dibuat beberapa milisaat yang lalu. Berikut adalah contoh output program apabila dipanggil pada baris arahan:

ABBBBBBBBBBBBBBBBBBDJJJJJJJJJJJJJJJJJJJJEGHHHHHHHHHHHHHHHHHHSSSSSSSSSSSSSS IICCBBBBBBBBBBBBBBBBBKBDLJMBOPLQNRTPHHHHHHHHFFFFFFFFFFFFSSSSSSSSSSSSSSSSSS FFFFFFFFFFFFFFFAAAAAACCCCCCCKKKKKKKKKKKKKKKKKMMMMMMMMMMMMMMMMMMMMMMMMMMMDD JEOQQQQQQQQQQQQQQQRRRRRRRRRRRRRRRRRBBBBBBBBBBBBBBBTTTTTTTTTTTTTTTTLPPPPPPP PPPPGGHHHFFFFFFFFIIIIIIIIIIIIIISSSSSSSSSSSSSSSSSSSSACCCCCCCCCCCCCCCCCCCKMD QQQQQQNNNNNNNNNNNNNNNNRRRRRTRRHHHHHHHHHFFFFFFFFFFFFFFFFFFIIIIIIIIIIIIIIIII MMMJEEEEEEEEEEDDDDEEEEEEEEEOOOOOOOOOOOOOOOOOOOOOOOOOOOQNNNNNNNNBTLPLRGFFFF FFFFFFFFFIIAAAAAAAAAAAAAAAAASSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKCCCCCCCMMJAA AACBean rosak! x = 67, y = 13 OOOOOOOOOOOOOOOOOO

Hasilnya menunjukkan 20 utas berjalan selari (sejauh pemerhati manusia); setiap utas menggunakan ID yang diterima pada waktu pembinaan untuk mencetak salah satu huruf A hingga T , 20 huruf pertama abjad. Sebaik sahaja ada benang yang mengetahui bahawa sifat membaca semula spottidak sesuai dengan ciri x = y yang diprogramkan, utas itu akan mencetak mesej "Bean rosak" dan menghentikan percubaan.

Apa yang anda lihat adalah kesan sampingan yang merosakkan dari keadaan perlumbaan dalam setSpot()kod kacang . Inilah kaedahnya sekali lagi:

public void setSpot (Titik titik) {// 'spot' setter this.x = point.x; ini.y = point.y; }

Apa yang mungkin salah dalam kod sebegitu mudah? Bayangkan thread A memanggil setSpot()dengan argumen titik sama dengan (67,67). Sekiranya kita sekarang memperlahankan jam Alam Semesta sehingga kita dapat melihat mesin maya Java (JVM) melaksanakan setiap pernyataan Java, satu demi satu, kita dapat membayangkan thread A yang melaksanakan penyata salinan koordinat x ( this.x = point.x;) dan kemudian, tiba-tiba, utas A dibekukan oleh sistem operasi, dan utas C dijadualkan berjalan sebentar. Dalam keadaan berjalan sebelumnya, utas C baru saja membuat titik rawak baru (13,13), disebut setSpot()sendiri, dan kemudian dibekukan untuk memberi ruang untuk benang M, tepat setelah ia menetapkan koordinat x ke 13. Oleh itu, benang yang dilanjutkan C sekarang diteruskan dengan logiknya yang diprogramkan: menetapkan y hingga 13 dan memeriksa sama ada harta tanah sama (13, 13), tetapi mendapati ia mempunyai misteri berubah menjadi keadaan haram (67, 13); koordinat x menjadi separuh keadaan utas A yang ditetapkan spot, dan koordinat y menjadi separuh keadaan utas benang C yang telah ditetapkanspot . Hasil akhirnya ialah kacang BrokenProperties berakhir dengan keadaan yang tidak konsisten secara dalaman: harta yang rosak.

Apabila struktur data bukan atom (iaitu struktur yang terdiri daripada lebih dari satu bahagian) dapat diubah oleh lebih dari satu utas pada satu masa, anda perlu melindungi struktur menggunakan kunci. Di Jawa, ini dilakukan dengan menggunakan synchronizedkata kunci.

Amaran: Tidak seperti semua jenis Java lain, perhatikan bahawa Java tidak menjaminnya longdan doublediperlakukan secara atom! Ini kerana longdan doublememerlukan 64 bit, iaitu dua kali panjang panjang perkataan seni bina CPU moden (32 bit). Kedua-dua memuatkan dan menyimpan kata mesin tunggal adalah operasi atom secara intrinsik, tetapi entiti 64-bit yang bergerak memerlukan dua pergerakan seperti itu, dan ini tidak dilindungi oleh Java kerana alasan biasa: prestasi. (Sebilangan CPU membenarkan bas sistem dikunci untuk melakukan pemindahan multi-kata secara atom, tetapi kemudahan ini tidak tersedia di semua CPU dan, dalam keadaan apa pun, sangat mahal untuk digunakan untuk semua longatau doublemanipulasi!) Jadi, walaupun harta terdiri hanya satu longatau satudouble, anda harus menggunakan langkah berjaga-jaga pengunci penuh untuk melindungi rindu atau rangkap anda daripada tiba-tiba rosak sepenuhnya.

Kata synchronizedkunci menandakan sekatan kod sebagai langkah atom. Kod tidak boleh "dibahagi", seperti ketika utas lain mengganggu kod untuk berpotensi memasuki semula yang menyekat dirinya sendiri (maka istilah kod reentrant ; semua kod Java harus masuk kembali). Penyelesaian untuk kacang BrokenProperties kami adalah remeh: ganti setSpot()kaedahnya dengan ini:

public void setSpot (Titik titik) {// 'spot' setter diselaraskan (ini) {this.x = point.x; ini.y = point.y; }}

Atau sebagai alternatif dengan ini:

setSpot void yang disegerakkan awam (Titik titik) {// 'spot' setter this.x = point.x; ini.y = point.y; }

Kedua-dua pengganti itu setara, walaupun saya lebih suka gaya pertama kerana menunjukkan dengan lebih jelas fungsi sebenar synchronizedkata kunci: blok segerak selalu dihubungkan dengan objek yang terkunci. Dengan terkunci, saya bermaksud bahawa JVM terlebih dahulu berusaha mendapatkan kunci (iaitu, akses eksklusif) pada objek (iaitu, mendapatkan akses eksklusif ke dalamnya), atau menunggu sehingga objek menjadi tidak terkunci jika telah dikunci oleh utas lain. Proses penguncian menjamin bahawa objek apa pun hanya dapat dikunci (atau dimiliki) oleh satu utas pada satu masa.

Jadi, synchronized (this)sintaks jelas mengetengahkan mekanisme dalaman: Argumen di dalam kurungan adalah objek yang akan dikunci (objek semasa) sebelum blok kod dimasukkan. Sintaks alternatif, di mana synchronizedkata kunci digunakan sebagai pengubah dalam tandatangan kaedah, hanyalah versi yang pendek dari yang pertama.

Amaran: Apabila kaedah statik ditandai synchronized, tidak ada thisobjek untuk dikunci; hanya kaedah contoh yang berkaitan dengan objek semasa. Oleh itu, apabila kaedah kelas diselaraskan, java.lang.Classobjek yang sesuai dengan kelas kaedah itu digunakan untuk mengunci. Pendekatan ini mempunyai implikasi prestasi yang serius kerana sekumpulan contoh kelas berkongsi satu Classobjek yang berkaitan ; setiap kali Classobjek itu terkunci, semua objek kelas itu (sama ada 3, 50, atau 1000!) dilarang menggunakan kaedah statik yang sama. Dengan ini, anda harus berfikir dua kali sebelum menggunakan penyegerakan dengan kaedah statik.

Dalam praktiknya, selalu ingat bentuk penyegerakan yang jelas kerana ia membolehkan anda "mengabomkan" sekumpulan sekecil mungkin kod dalam satu kaedah. Bentuk singkatan "atomized" keseluruhan kaedah, yang, atas sebab prestasi, selalunya tidak seperti yang anda mahukan. Sebaik sahaja thread yang telah memasuki blok atom kod, tiada thread lain yang perlu untuk melaksanakan mana-mana kod disegerakkan pada objek yang sama boleh berbuat demikian.

Petua: Apabila kunci diperoleh pada objek, maka semua kod yang disegerakkan untuk kelas objek itu akan menjadi atom. Oleh itu, jika kelas anda mengandungi lebih daripada satu struktur data yang perlu dirawat secara atomik, tetapi struktur data tersebut tidak bergantung antara satu sama lain, maka masalah yang lain boleh timbul. Pelanggan memanggil kaedah penyegerakan yang memanipulasi satu struktur data dalaman akan menyekat semua pelanggan lain yang memanggil kaedah lain yang menangani struktur data atom lain dari kelas anda. Jelas sekali, anda harus mengelakkan situasi seperti itu dengan memisahkan kelas menjadi kelas yang lebih kecil yang hanya mengendalikan satu struktur data untuk dirawat secara atom pada satu masa.

The JVM implements its synchronization feature by creating queues of threads waiting for an object to become unlocked. While this strategy is great when it comes to protecting the consistency of composite data structures, it can result in multithreaded traffic jams when a less-than-efficient section of code is marked as synchronized.

Therefore, always pay attention to just how much code you synchronize: it should be the absolute minimum necessary. For example, imagine our setSpot() method originally consisted of:

public void setSpot(Point point) { // 'spot' setter log.println("setSpot() called on " + this.toString() ); this.x = point.x; this.y = point.y; } 

Walaupun printlnpernyataan tersebut mungkin masuk dalam kaedah, secara logiksetSpot() bukan bahagian dari urutan penyataan yang perlu dikelompokkan menjadi satu atom keseluruhan. Oleh itu, dalam kes ini, cara yang betul untuk menggunakan synchronizedkata kunci adalah seperti berikut:

public void setSpot (Titik titik) {// 'spot' setter log.println ("setSpot () dipanggil" + this.toString ()); disegerakkan (ini) {this.x = point.x; ini.y = point.y; }}

Cara "malas", dan pendekatan yang harus anda hindari, kelihatan seperti ini: