JavaBeans: properti, acara, dan keamanan thread

Java adalah bahasa dinamis yang menyertakan konstruksi bahasa multithreading yang mudah digunakan dan kelas pendukung. Banyak program Java mengandalkan multithreading untuk memanfaatkan paralelisme aplikasi internal, meningkatkan kinerja jaringan, atau mempercepat tanggapan umpan balik pengguna. Sebagian besar waktu proses Java menggunakan multithreading untuk mengimplementasikan fitur pengumpulan sampah Java. Terakhir, AWT juga bergantung pada utas terpisah untuk fungsinya. Singkatnya, bahkan program Java yang paling sederhana pun lahir dalam lingkungan multithreading yang aktif.

Kacang Java, oleh karena itu, juga digunakan dalam lingkungan multithread yang dinamis, dan di sinilah letak bahaya klasik menghadapi kondisi balapan . Kondisi balapan adalah skenario aliran program yang bergantung pada waktu yang dapat menyebabkan kerusakan status (data program). Pada bagian berikut saya akan merinci dua skenario tersebut. Setiap kacang Java perlu dirancang dengan mempertimbangkan kondisi perlombaan sehingga kacang dapat bertahan untuk digunakan secara bersamaan oleh beberapa utas klien.

Masalah multithreading dengan properti sederhana

Implementasi bean harus mengasumsikan bahwa banyak thread sedang mengakses dan / atau memodifikasi instance bean tunggal pada waktu yang sama. Sebagai contoh kacang yang diimplementasikan secara tidak benar (yang berkaitan dengan kesadaran multithreading), pertimbangkan kacang BrokenProperties berikut dan program pengujian MTProperties terkait:

BrokenProperties.java

impor java.awt.Point;

// Demo Bean yang tidak melindungi penggunaan banyak utas.

public class BrokenProperties extends Point {

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

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

} public Point getSpot () {// 'spot' getter mengembalikan ini; }} // Akhir Bean / Kelas BrokenProperties

MTProperties.java

impor java.awt.Point; utilitas impor. *; import utilities.beans. *;

public class MTProperties extends Thread {

dilindungi BrokenProperties myBean; // target bean to bash ..

dilindungi int myID; // setiap utas membawa sedikit ID

// ------------------------------------------------ ------------------- // titik masuk main () // ---------------------- --------------------------------------------- public static void main ( String [] args) {

Kacang BrokenProperties; Benang benang;

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

for (int i = 0; i <20; i ++) {// start 20 threads to bash bean thread = new MTProperties (bean, i); // utas mendapatkan akses ke bean thread.start (); }} // ---------------------------------------------- --------------------- // MTProperties Constructor // ----------------------- --------------------------------------------

MTProperties publik (BrokenProperties bean, int id) {this.myBean = bean; // perhatikan kacang untuk mengatasi this.myID = id; // perhatikan siapa kami} // ----------------------------------------- -------------------------- // loop utama utas: // lakukan selamanya // buat Titik acak baru dengan x == y // beri tahu bean untuk mengadopsi Point sebagai properti 'spot' barunya // tanya bean apa properti 'spot'-nya sekarang disetel ke // melempar goyah jika spot x tidak sama dengan spot y // --------- -------------------------------------------------- -------- public void run () {int someInt; Titik titik = Titik baru ();

while (true) {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 rusak! x =" + point.x + ", y =" + point.y); System.exit (10); } System.out.print ((char) ('A' + myID)); System.out.flush (); }}} // Akhir dari MTProperti Kelas

Catatan: Paket utilitas yang diimpor oleh MTPropertiesberisi kelas yang dapat digunakan kembali dan metode statis yang dikembangkan untuk buku oleh penulis.

Dua daftar kode sumber di atas mendefinisikan kacang yang disebut BrokenProperties dan kelas MTProperties, yang digunakan untuk melatih kacang dari dalam 20 thread yang sedang berjalan. Mari kita ikuti MTProperties' main()entry point: Pertama itu membuat instance kacang BrokenProperties, diikuti dengan pembuatan dan permulaan 20 utas. Kelas MTPropertiesmeluas java.lang.Thread, sehingga semua yang perlu kita lakukan untuk mengubah kelas MTPropertiesmenjadi benang adalah untuk kelas override Thread's run()metode. Konstruktor untuk utas kami mengambil dua argumen: objek kacang yang akan berkomunikasi dengan utas dan identifikasi unik, yang memungkinkan 20 utas dapat dengan mudah dibedakan pada waktu proses.

Akhir bisnis dari demo ini adalah run()metode kami di kelas MTProperties. Di sini kita mengulang selamanya, membuat titik baru (x, y) acak, tetapi dengan karakteristik berikut: koordinat x mereka selalu sama dengan koordinat y mereka. Titik-titik acak ini diteruskan ke setSpot()metode penyetel kacang dan kemudian segera dibaca kembali menggunakan getSpot()metode pengambil. Anda akan mengharapkan spotproperti read menjadi identik dengan titik acak yang dibuat beberapa milidetik yang lalu. Berikut adalah contoh keluaran program ketika dipanggil di baris perintah:

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

Outputnya menunjukkan 20 thread yang berjalan secara paralel (sejauh yang bisa dilakukan pengamat manusia); setiap utas menggunakan ID yang diterima pada waktu konstruksi untuk mencetak salah satu huruf A hingga T , 20 huruf pertama alfabet. Segera setelah utas menemukan bahwa spotproperti baca kembali tidak sesuai dengan karakteristik terprogram x = y, utas akan mencetak pesan "Kacang rusak" dan menghentikan percobaan.

Apa yang Anda lihat adalah efek samping yang merusak status dari kondisi balapan di dalam setSpot()kode kacang . Inilah metode itu lagi:

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

Apa yang bisa salah dalam kode sederhana seperti itu? Bayangkan thread A memanggil setSpot()dengan argumen titik sama dengan (67,67). Jika sekarang kita memperlambat jam Semesta sehingga kita dapat melihat mesin virtual Java (JVM) mengeksekusi setiap pernyataan Java, satu per satu, kita dapat membayangkan thread A menjalankan pernyataan salinan koordinat x ( this.x = point.x;) dan kemudian, tiba-tiba, utas A dibekukan oleh sistem operasi, dan utas C dijadwalkan untuk berjalan untuk sementara waktu. Dalam status berjalan sebelumnya, utas C baru saja membuat titik acak barunya sendiri (13,13), memanggil setSpot()dirinya sendiri, dan kemudian dibekukan untuk memberi ruang bagi utas M, tepat setelah itu menyetel koordinat x ke 13. Jadi, utas C yang dilanjutkan sekarang melanjutkan dengan logika terprogramnya: menyetel y ke 13 dan memeriksa apakah properti spot sama (13, 13), tetapi menemukan bahwa ia memiliki secara misterius berubah menjadi keadaan ilegal (67, 13); koordinat x menjadi setengah keadaan apa benang A terbenam spotke, dan y koordinat yang setengah keadaan apa benang C telah menetapkanspot untuk. Hasil akhirnya adalah bahwa kacang BrokenProperties berakhir dengan status yang tidak konsisten secara internal: properti rusak.

Kapan pun struktur data non-atom (yaitu, struktur yang terdiri dari lebih dari satu bagian) dapat dimodifikasi oleh lebih dari satu utas pada satu waktu, Anda perlu melindungi struktur menggunakan kunci. Di Java, ini dilakukan dengan menggunakan synchronizedkata kunci.

Peringatan: Tidak seperti semua tipe Java lainnya, perhatikan bahwa Java tidak menjamin itu longdan doublediperlakukan secara atomik! Ini karena longdan doublemembutuhkan 64 bit, yang dua kali panjang panjang kata kebanyakan arsitektur CPU modern (32 bit). Baik memuat dan menyimpan kata mesin tunggal pada dasarnya adalah operasi atom, tetapi memindahkan entitas 64-bit memerlukan dua gerakan seperti itu, dan ini tidak dilindungi oleh Java karena alasan yang biasa: kinerja. (Beberapa CPU mengizinkan bus sistem dikunci untuk melakukan transfer multi kata secara atomik, tetapi fasilitas ini tidak tersedia di semua CPU dan, bagaimanapun, akan menjadi sangat mahal untuk diterapkan ke semua longatau doublemanipulasi!) Jadi, bahkan ketika sebuah properti terdiri hanya satu longatau satudouble, Anda harus menggunakan tindakan pencegahan penguncian penuh untuk melindungi long atau doubles Anda dari kerusakan total yang tiba-tiba.

Kata synchronizedkunci menandai blok kode sebagai langkah atom. Kode tidak dapat "dibagi", seperti ketika thread lain menyela kode untuk kemungkinan masuk kembali ke blok itu sendiri (karena itu istilah kode reentrant ; semua kode Java harus reentrant). Solusi untuk kacang BrokenProperties kami sepele: ganti saja setSpot()metodenya dengan ini:

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

Atau alternatifnya dengan ini:

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

Kedua penggantian sama persis, meskipun saya lebih suka gaya pertama karena gaya tersebut menunjukkan dengan lebih jelas apa fungsi sebenarnya dari synchronizedkata kunci tersebut: blok tersinkronisasi selalu ditautkan ke objek yang terkunci. Yang saya maksud dengan terkunci adalah JVM pertama kali mencoba untuk mendapatkan kunci (yaitu, akses eksklusif) pada objek (yaitu, memperoleh akses eksklusif ke objek tersebut), atau menunggu hingga objek menjadi tidak terkunci jika telah dikunci oleh utas lain. Proses penguncian menjamin bahwa objek apa pun hanya dapat dikunci (atau dimiliki) oleh satu utas dalam satu waktu.

Jadi, synchronized (this)sintaksnya dengan jelas menggemakan mekanisme internal: Argumen di dalam tanda kurung adalah objek yang akan dikunci (objek saat ini) sebelum blok kode dimasukkan. Sintaks alternatif, di mana synchronizedkata kunci digunakan sebagai pengubah dalam tanda tangan metode, hanyalah singkatan dari versi sebelumnya.

Peringatan: Ketika metode statis ditandai synchronized, tidak ada thisobjek untuk dikunci; hanya metode instance yang dikaitkan dengan objek saat ini. Jadi, ketika metode kelas disinkronkan, java.lang.Classobjek yang sesuai dengan kelas metode itu akan digunakan untuk mengunci. Pendekatan ini memiliki implikasi kinerja yang serius karena kumpulan instance kelas berbagi satu Classobjek terkait ; setiap kali Classobjek itu terkunci, semua objek kelas itu (apakah 3, 50, atau 1000!) dilarang menjalankan metode statis yang sama. Dengan pemikiran ini, Anda harus berpikir dua kali sebelum menggunakan sinkronisasi dengan metode statis.

Dalam praktiknya, selalu ingat formulir tersinkronisasi eksplisit karena memungkinkan Anda untuk "menyemprotkan" blok kode sekecil mungkin dalam suatu metode. Bentuk singkatan "menyatukan" seluruh metode, yang, karena alasan kinerja, seringkali bukan yang Anda inginkan. Setelah utas memasuki blok kode atom, tidak ada utas lain yang perlu menjalankan kode tersinkron apa pun pada objek yang sama yang dapat melakukannya.

Tip: Saat kunci diperoleh pada suatu objek, semua kode yang disinkronkan untuk kelas objek itu akan menjadi atom. Oleh karena itu, jika kelas Anda berisi lebih dari satu struktur data yang perlu diperlakukan secara atomis, tetapi struktur data tersebut tidak bergantung satu sama lain, maka kemacetan kinerja lain dapat muncul. Klien yang memanggil metode tersinkronisasi yang memanipulasi satu struktur data internal akan memblokir semua klien lain yang memanggil metode lain yang menangani struktur data atomik lainnya di kelas Anda. Jelas, Anda harus menghindari situasi seperti itu dengan membagi kelas menjadi kelas-kelas yang lebih kecil yang hanya menangani satu struktur data untuk diperlakukan secara atom pada satu waktu.

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; } 

Meskipun printlnpernyataan secara logis mungkin termasuk dalam setSpot()metode, itu bukanlah bagian dari urutan pernyataan yang perlu dikelompokkan menjadi keseluruhan atom. Oleh karena itu, dalam hal ini, cara yang benar untuk menggunakan synchronizedkata kunci tersebut adalah sebagai berikut:

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

Cara "malas", dan pendekatan yang harus Anda hindari, terlihat seperti ini: