Desain untuk keamanan benang

Enam bulan lalu saya memulai serangkaian artikel tentang mendesain kelas dan objek. Di kolom Design Techniques bulan ini , saya akan melanjutkan seri tersebut dengan melihat prinsip-prinsip desain yang memperhatikan keamanan benang. Artikel ini memberi tahu Anda apa itu keamanan thread, mengapa Anda membutuhkannya, kapan Anda membutuhkannya, dan bagaimana cara mendapatkannya.

Apa keamanan benang?

Keamanan thread secara sederhana berarti bahwa bidang-bidang objek atau kelas selalu mempertahankan status yang valid, seperti yang diamati oleh objek dan kelas lain, bahkan ketika digunakan secara bersamaan oleh beberapa utas.

Salah satu pedoman pertama yang saya usulkan dalam kolom ini (lihat "Mendesain inisialisasi objek") adalah bahwa Anda harus mendesain kelas sedemikian rupa sehingga objek mempertahankan status yang valid, dari awal masa pakai hingga akhir. Jika Anda mengikuti saran ini dan membuat objek yang semua variabel instansnya privat dan yang metodenya hanya membuat transisi status yang tepat pada variabel instans tersebut, Anda berada dalam kondisi yang baik dalam lingkungan single-threaded. Tetapi Anda mungkin mendapat masalah saat lebih banyak utas muncul.

Beberapa utas dapat mengeja masalah untuk objek Anda karena seringkali, saat sebuah metode sedang dalam proses eksekusi, status objek Anda bisa sementara tidak valid. Ketika hanya satu utas yang memanggil metode objek, hanya satu metode pada satu waktu yang akan dieksekusi, dan setiap metode akan diizinkan untuk selesai sebelum metode lain dipanggil. Jadi, dalam lingkungan single-threaded, setiap metode akan diberi kesempatan untuk memastikan bahwa setiap status tidak valid sementara diubah menjadi status valid sebelum metode kembali.

Namun, setelah Anda memasukkan beberapa utas, JVM dapat mengganggu utas yang menjalankan satu metode sementara variabel instance objek masih dalam status tidak valid untuk sementara. JVM kemudian dapat memberikan kesempatan untuk mengeksekusi thread yang berbeda, dan thread tersebut dapat memanggil metode pada objek yang sama. Semua kerja keras Anda untuk menjadikan variabel instance pribadi dan metode Anda hanya melakukan transformasi status yang valid tidak akan cukup untuk mencegah thread kedua ini mengamati objek dalam status tidak valid.

Objek seperti itu tidak akan aman untuk thread, karena dalam lingkungan multithread, objek dapat rusak atau dianggap memiliki status tidak valid. Objek thread-safe adalah objek yang selalu mempertahankan status valid, seperti yang diamati oleh objek dan kelas lain, bahkan di lingkungan multithread.

Mengapa mengkhawatirkan keamanan benang?

Ada dua alasan besar mengapa Anda perlu memikirkan keamanan thread saat mendesain class dan objek di Java:

  1. Dukungan untuk banyak utas dibangun ke dalam bahasa Java dan API

  2. Semua utas di dalam mesin virtual Java (JVM) berbagi area heap dan metode yang sama

Karena multithreading dibangun ke dalam Java, ada kemungkinan bahwa setiap kelas yang Anda rancang pada akhirnya dapat digunakan secara bersamaan oleh beberapa utas. Anda tidak perlu (dan tidak seharusnya) membuat setiap kelas yang Anda desain aman untuk thread, karena keamanan thread tidak gratis. Tetapi Anda setidaknya harus memikirkan keamanan thread setiap kali Anda mendesain kelas Java. Anda akan menemukan pembahasan tentang biaya keamanan thread dan pedoman tentang kapan harus membuat kelas aman untuk thread nanti di artikel ini.

Mengingat arsitektur JVM, Anda hanya perlu memperhatikan variabel instance dan kelas saat mengkhawatirkan keamanan thread. Karena semua utas berbagi heap yang sama, dan heap adalah tempat semua variabel instance disimpan, beberapa utas dapat mencoba menggunakan variabel instance objek yang sama secara bersamaan. Demikian juga, karena semua utas berbagi area metode yang sama, dan area metode adalah tempat semua variabel kelas disimpan, beberapa utas dapat mencoba menggunakan variabel kelas yang sama secara bersamaan. Ketika Anda memilih untuk membuat kelas thread-safe, tujuan Anda adalah untuk menjamin integritas - dalam lingkungan multithread - dari variabel instance dan kelas yang dideklarasikan di kelas itu.

Anda tidak perlu khawatir tentang akses multithread ke variabel lokal, parameter metode, dan nilai kembalian, karena variabel ini berada di tumpukan Java. Di JVM, setiap utas diberikan tumpukan Java-nya sendiri. Tidak ada utas yang dapat melihat atau menggunakan variabel lokal, nilai kembalian, atau parameter milik utas lain.

Mengingat struktur JVM, variabel lokal, parameter metode, dan nilai kembalian secara inheren "aman untuk thread". Tetapi variabel instance dan variabel kelas hanya akan menjadi thread-safe jika Anda mendesain kelas dengan tepat.

RGBColor # 1: Siap untuk satu utas

Sebagai contoh kelas yang tidak aman untuk thread, pertimbangkan RGBColorkelas tersebut, yang ditunjukkan di bawah ini. Contoh kelas ini mewakili warna disimpan dalam tiga variabel misalnya swasta: r, g, dan b. Dengan kelas yang ditunjukkan di bawah ini, sebuah RGBColorobjek akan memulai hidupnya dalam keadaan yang valid dan hanya akan mengalami transisi status-valid, dari awal masa pakainya sampai akhir - tetapi hanya dalam lingkungan berulir tunggal.

// Dalam utas file / ex1 / RGBColor.java // Instance dari kelas ini TIDAK aman untuk utas. kelas publik RGBColor {private int r; int pribadi g; private int b; publik RGBColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; ini.g = g; this.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; ini.g = g; this.b = b; } / ** * mengembalikan warna dalam larik tiga int: R, G, dan B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; kembali retVal; } public void invert () {r = 255 - r; g = 255 - g; b = 255 - b; } private static void checkRGBVals (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {throw new IllegalArgumentException (); }}}

Karena tiga variabel instan, ints r, gdan b, bersifat privat, satu-satunya cara kelas dan objek lain dapat mengakses atau mempengaruhi nilai variabel ini adalah melalui RGBColorkonstruktor dan metode. Desain konstruktor dan metode menjamin bahwa:

  1. RGBColorkonstruktor akan selalu memberikan nilai awal variabel yang tepat

  2. Metode setColor()dan invert()akan selalu melakukan transformasi status yang valid pada variabel ini

  3. Metode getColor()akan selalu mengembalikan tampilan yang valid dari variabel ini

Perhatikan bahwa jika data buruk diteruskan ke konstruktor atau setColor()metode, mereka akan menyelesaikannya secara tiba-tiba dengan InvalidArgumentException. The checkRGBVals()metode, yang melempar pengecualian ini, pada dasarnya mendefinisikan apa artinya untuk RGBColorobjek yang akan berlaku: nilai-nilai dari ketiga variabel, r, g, dan b, harus antara 0 dan 255, inklusif. Selain itu, agar valid, warna yang diwakili oleh variabel-variabel ini harus merupakan warna terbaru yang diteruskan ke konstruktor atau setColor()metode, atau dihasilkan oleh invert()metode.

Jika, dalam lingkungan single-threaded, Anda memanggil setColor()dan meneruskan dengan warna biru, RGBColorobjek akan menjadi biru saat setColor()dikembalikan. Jika Anda kemudian memanggil getColor()objek yang sama, Anda akan mendapatkan warna biru. Dalam masyarakat single-threaded, contoh RGBColorkelas ini berperilaku baik.

Melempar kunci pas bersamaan ke dalam pekerjaan

Sayangnya, gambar bahagia dari RGBColorobjek yang berperilaku baik ini dapat berubah menjadi menakutkan ketika utas lain memasuki gambar. Dalam lingkungan multithread, instance RGBColorkelas yang ditentukan di atas rentan terhadap dua jenis perilaku buruk: konflik tulis / tulis dan konflik baca / tulis.

Konflik tulis / tulis

Bayangkan Anda memiliki dua utas, satu utas bernama "merah" dan lainnya bernama "biru." Kedua utas mencoba menyetel warna RGBColorobjek yang sama : Benang merah mencoba menyetel warna menjadi merah; benang biru mencoba menyetel warna menjadi biru.

Kedua utas ini mencoba menulis ke variabel instance objek yang sama secara bersamaan. Jika penjadwal utas menyisipkan kedua utas ini dengan cara yang benar, kedua utas akan secara tidak sengaja saling mengganggu, menghasilkan konflik tulis / tulis. Dalam prosesnya, dua utas akan merusak status objek.

The UnsynchronizedRGBColor applet

Applet berikut, bernama Unsynchronized RGBColor , mendemonstrasikan satu urutan kejadian yang dapat mengakibatkan RGBColorobjek rusak . Benang merah dengan polosnya mencoba mengatur warna menjadi merah sementara benang biru dengan polosnya mencoba mengatur warna menjadi biru. Pada akhirnya, RGBColorobjek tersebut tidak mewakili merah atau biru tetapi warna yang mengganggu, magenta.

Untuk beberapa alasan, browser Anda tidak akan membiarkan Anda melihat applet Java yang keren seperti ini.

Untuk menelusuri urutan kejadian yang mengarah ke RGBColorobjek rusak , tekan tombol Langkah pada applet. Tekan Kembali untuk mencadangkan langkah, dan Atur Ulang untuk mundur ke awal. Saat Anda melanjutkan, sebaris teks di bagian bawah applet akan menjelaskan apa yang terjadi selama setiap langkah.

Bagi Anda yang tidak bisa menjalankan applet, berikut tabel yang menunjukkan urutan kejadian yang ditunjukkan oleh applet:

Benang Pernyataan r g b Warna
tidak ada objek mewakili warna hijau 0 255 0  
biru benang biru memanggil setColor (0, 0, 255) 0 255 0  
biru checkRGBVals(0, 0, 255); 0 255 0  
biru this.r = 0; 0 255 0  
biru this.g = 0; 0 255 0  
biru biru didahului 0 0 0  
merah benang merah memanggil setColor (255, 0, 0) 0 0 0  
merah checkRGBVals(255, 0, 0); 0 0 0  
merah this.r = 255; 0 0 0  
merah this.g = 0; 255 0 0  
merah this.b = 0; 255 0 0  
merah benang merah kembali 255 0 0  
biru kemudian, benang biru berlanjut 255 0 0  
biru this.b = 255 255 0 0  
biru benang biru kembali 255 0 255  
tidak ada objek mewakili magenta 255 0 255  

Seperti yang dapat Anda lihat dari applet dan tabel ini, RGBColorini rusak karena penjadwal utas menyela utas biru sementara objek masih dalam keadaan tidak valid untuk sementara. Ketika benang merah masuk dan mengecat objek dengan warna merah, benang biru hanya selesai mengecat objek menjadi biru. Ketika benang biru kembali untuk menyelesaikan pekerjaan, itu secara tidak sengaja merusak objek.

Konflik baca / tulis

Another kind of misbehavior that may be exhibited in a multithreaded environment by instances of this RGBColor class is read/write conflicts. This kind of conflict arises when an object's state is read and used while in a temporarily invalid state due to the unfinished work of another thread.

For example, note that during the blue thread's execution of the setColor() method above, the object at one point finds itself in the temporarily invalid state of black. Here, black is a temporarily invalid state because:

  1. It is temporary: Eventually, the blue thread intends to set the color to blue.

  2. It is invalid: No one asked for a black RGBColor object. The blue thread is supposed to turn a green object into blue.

If the blue thread is preempted at the moment the object represents black by a thread that invokes getColor() on the same object, that second thread would observe the RGBColor object's value to be black.

Here's a table that shows a sequence of events that could lead to just such a read/write conflict:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes getColor() 0 0 0  
red int[] retVal = new int[3]; 0 0 0  
red retVal[0] = 0; 0 0 0  
red retVal[1] = 0; 0 0 0  
red retVal[2] = 0; 0 0 0  
red return retVal; 0 0 0  
red red thread returns black 0 0 0  
blue later, blue thread continues 0 0 0  
blue this.b = 255 0 0 0  
blue blue thread returns 0 0 255  
none object represents blue 0 0 255  

As you can see from this table, the trouble begins when the blue thread is interrupted when it has only partially finished painting the object blue. At this point the object is in a temporarily invalid state of black, which is exactly what the red thread sees when it invokes getColor() on the object.

Three ways to make an object thread-safe

There are basically three approaches you can take to make an object such as RGBThread thread-safe:

  1. Synchronize critical sections
  2. Make it immutable
  3. Use a thread-safe wrapper

Approach 1: Synchronizing the critical sections

The most straightforward way to correct the unruly behavior exhibited by objects such as RGBColor when placed in a multithreaded context is to synchronize the object's critical sections. An object's critical sections are those methods or blocks of code within methods that must be executed by only one thread at a time. Put another way, a critical section is a method or block of code that must be executed atomically, as a single, indivisible operation. By using Java's synchronized keyword, you can guarantee that only one thread at a time will ever execute the object's critical sections.

To take this approach to making your object thread-safe, you must follow two steps: you must make all relevant fields private, and you must identify and synchronize all the critical sections.

Step 1: Make fields private

Sinkronisasi berarti bahwa hanya satu utas pada satu waktu yang dapat mengeksekusi sedikit kode (bagian kritis). Jadi meskipun itu adalah bidang yang ingin Anda akses untuk mengoordinasikan di antara beberapa utas, mekanisme Java untuk melakukannya sebenarnya mengoordinasikan akses ke kode. Ini berarti bahwa hanya jika Anda membuat data menjadi pribadi, Anda akan dapat mengontrol akses ke data tersebut dengan mengontrol akses ke kode yang memanipulasi data.