Reka bentuk untuk keselamatan benang

Enam bulan yang lalu saya memulakan siri artikel mengenai merancang kelas dan objek. Di ruangan Teknik Reka Bentuk bulan ini , saya akan meneruskan siri tersebut dengan melihat prinsip reka bentuk yang mementingkan keselamatan benang. Artikel ini memberitahu anda apa itu keselamatan benang, mengapa anda memerlukannya, ketika anda memerlukannya, dan bagaimana cara mendapatkannya.

Apakah keselamatan benang?

Keselamatan benang bermaksud bahawa medan objek atau kelas selalu mempertahankan keadaan yang sah, seperti yang diperhatikan oleh objek dan kelas lain, bahkan ketika digunakan secara serentak oleh beberapa utas.

Salah satu garis panduan pertama yang saya cadangkan dalam ruangan ini (lihat "Merancang inisialisasi objek") ialah anda harus merancang kelas sedemikian rupa sehingga objek dapat mengekalkan keadaan yang sah, dari awal masa hidupnya hingga akhir. Sekiranya anda mengikuti nasihat ini dan membuat objek yang pemboleh ubah contohnya semuanya bersifat peribadi dan kaedahnya hanya membuat peralihan keadaan yang tepat pada pemboleh ubah contoh tersebut, anda berada dalam keadaan baik dalam lingkungan satu utas. Tetapi anda mungkin menghadapi masalah apabila terdapat lebih banyak utas.

Beberapa utas dapat menimbulkan masalah pada objek anda kerana selalunya, sementara suatu kaedah sedang dalam proses pelaksanaan, keadaan objek Anda dapat menjadi sementara tidak valid. Apabila hanya satu utas yang menggunakan kaedah objek, hanya satu kaedah pada satu masa yang akan dijalankan, dan setiap kaedah akan dibiarkan selesai sebelum kaedah lain dipanggil. Oleh itu, dalam lingkungan utas tunggal, setiap kaedah akan diberi peluang untuk memastikan bahawa keadaan tidak sah sementara diubah menjadi keadaan yang sah sebelum kaedah kembali.

Sebaik sahaja anda memperkenalkan banyak utas, JVM mungkin mengganggu utas menjalankan satu kaedah sementara pemboleh ubah contoh objek masih dalam keadaan tidak sah sementara. JVM kemudian dapat memberi peluang pada thread yang lain untuk dijalankan, dan thread tersebut dapat memanggil metode pada objek yang sama. Semua kerja keras anda untuk menjadikan pemboleh ubah contoh anda bersifat peribadi dan kaedah anda hanya melakukan transformasi keadaan yang sah tidak akan mencukupi untuk mengelakkan utas kedua ini memerhatikan objek dalam keadaan tidak sah.

Objek seperti itu tidak akan selamat untuk benang, kerana dalam lingkungan multithread, objek tersebut dapat menjadi rusak atau diperhatikan memiliki keadaan yang tidak valid. Objek yang selamat untuk utas adalah objek yang selalu mengekalkan keadaan yang sah, seperti yang diperhatikan oleh kelas dan objek lain, walaupun dalam lingkungan multithread.

Mengapa perlu risau keselamatan benang?

Terdapat dua sebab besar yang perlu anda fikirkan tentang keselamatan benang semasa anda merancang kelas dan objek di Java:

  1. Sokongan untuk pelbagai utas dimasukkan ke dalam bahasa dan API Java

  2. Semua utas di dalam mesin maya Java (JVM) berkongsi kawasan timbunan dan kaedah yang sama

Oleh kerana multithreading dibangun ke dalam Java, ada kemungkinan kelas yang anda reka akhirnya dapat digunakan secara serentak oleh beberapa utas. Anda tidak perlu (dan tidak seharusnya) membuat setiap kelas yang anda reka benang selamat, kerana keselamatan benang tidak datang secara percuma. Tetapi anda sekurang-kurangnya harus memikirkan keselamatan benang setiap kali anda merancang kelas Java. Anda akan menemui perbincangan mengenai kos keselamatan benang dan garis panduan mengenai waktu membuat kelas yang selamat untuk benang dalam artikel ini.

Memandangkan seni bina JVM, anda hanya perlu memperhatikan pemboleh ubah contoh dan kelas apabila anda bimbangkan keselamatan thread. Kerana semua utas mempunyai timbunan yang sama, dan timbunan adalah tempat semua pemboleh ubah contoh disimpan, beberapa utas dapat berusaha menggunakan pemboleh ubah contoh objek yang sama secara bersamaan. Begitu juga, kerana semua utas berkongsi kawasan kaedah yang sama, dan kawasan kaedah adalah tempat semua pemboleh ubah kelas disimpan, beberapa utas boleh berusaha menggunakan pemboleh ubah kelas yang sama secara serentak. Apabila anda memilih untuk menjadikan kelas lebih selamat, tujuan anda adalah untuk menjamin integriti - dalam persekitaran berbilang benang - contoh dan pemboleh ubah kelas yang dinyatakan dalam kelas tersebut.

Anda tidak perlu risau tentang akses multithreaded ke pemboleh ubah tempatan, parameter kaedah, dan nilai kembali, kerana pemboleh ubah ini berada di tumpukan Java. Di JVM, setiap utas diberikan stack Java sendiri. Tidak ada utas yang dapat melihat atau menggunakan pemboleh ubah tempatan, nilai kembali, atau parameter milik utas lain.

Memandangkan struktur JVM, pemboleh ubah tempatan, parameter kaedah, dan nilai pulangan sememangnya "selamat di dalam benang." Tetapi pemboleh ubah contoh dan pemboleh ubah kelas hanya akan selamat pada benang jika anda merancang kelas anda dengan betul.

RGBColor # 1: Bersedia untuk satu utas

Sebagai contoh kelas yang tidak selamat di benang, pertimbangkan RGBColorkelas yang ditunjukkan di bawah. Contoh kelas ini mewakili warna disimpan dalam tiga pembolehubah contoh swasta: r, g, dan b. Memandangkan kelas yang ditunjukkan di bawah ini, RGBColorobjek akan memulakan hidupnya dalam keadaan yang sah dan hanya akan mengalami peralihan keadaan yang sah, dari awal hayatnya hingga akhir - tetapi hanya dalam lingkungan satu utas.

// Dalam utas fail / ex1 / RGBColor.java // Contoh kelas ini TIDAK selamat untuk utas. kelas awam RGBColor {int int r; int g; int peribadi b; RGBColor awam (int r, int g, int b) {checkRGBVals (r, g, b); ini.r = r; ini.g = g; ini.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); ini.r = r; ini.g = g; ini.b = b; } / ** * mengembalikan warna dalam susunan tiga int: R, G, dan B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; pulangan retVal; } pembalikan kekosongan awam () {r = 255 - r; g = 255 - g; b = 255 - b; } periksa kekosongan statik persendirianRGBVals (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {lemparkan IllegalArgumentException baru (); }}}

Kerana tiga pembolehubah contoh, ints r, gdan b, adalah swasta, satu-satunya cara kelas dan objek lain boleh mengakses atau mempengaruhi nilai-nilai pembolehubah ini adalah melalui RGBColorpembina dan kaedah 's. Reka bentuk pembina dan kaedah menjamin bahawa:

  1. RGBColorPembina akan sentiasa memberikan pemboleh ubah nilai awal yang tepat

  2. Kaedah setColor()dan invert()akan selalu melakukan transformasi keadaan yang sah pada pemboleh ubah ini

  3. Kaedah getColor()akan selalu mengembalikan pandangan yang betul mengenai pemboleh ubah ini

Perhatikan bahawa jika data buruk dihantar ke konstruktor atau setColor()kaedahnya, data akan diselesaikan secara tiba-tiba dengan InvalidArgumentException. Yang checkRGBVals()kaedah, yang melemparkan pengecualian ini, dalam mentakrifkan kesan apa yang dimaksudkan dengan RGBColorobjek sah: nilai-nilai ketiga-tiga pembolehubah, r, g, dan b, mesti antara 0 dan 255, termasuk. Di samping itu, agar valid, warna yang ditunjukkan oleh pemboleh ubah ini mestilah warna paling baru sama ada diteruskan ke konstruktor atau setColor()kaedah, atau dihasilkan dengan invert()kaedah tersebut.

Sekiranya, dalam persekitaran utas tunggal, anda memanggil setColor()dan melewati dengan warna biru, RGBColorobjek akan berwarna biru ketika setColor()kembali. Sekiranya anda menggunakan getColor()objek yang sama, anda akan berwarna biru. Dalam masyarakat berangkai tunggal, contoh RGBColorkelas ini berkelakuan baik.

Melemparkan sepana bersamaan ke dalam karya

Malangnya, gambar gembira dari RGBColorobjek yang berkelakuan baik ini boleh menjadi menakutkan apabila benang lain memasuki gambar. Dalam persekitaran berbilang benang, contoh RGBColorkelas yang dinyatakan di atas terdedah kepada dua jenis tingkah laku buruk: konflik menulis / menulis dan konflik membaca / menulis.

Menulis / menulis konflik

Bayangkan anda mempunyai dua utas, satu utas diberi nama "merah" dan satu lagi diberi "biru". Kedua-dua utas cuba menetapkan warna RGBColorobjek yang sama : Benang merah cuba menetapkan warna menjadi merah; benang biru cuba menetapkan warna menjadi biru.

Kedua-dua utas ini cuba menulis kepada pemboleh ubah contoh objek yang sama secara serentak. Sekiranya penjadual utas melilit kedua utas ini dengan cara yang betul, kedua utas secara tidak sengaja akan saling mengganggu, menghasilkan konflik tulis / tulis. Dalam prosesnya, kedua utas akan merosakkan keadaan objek.

The UnsynchronizedRGBColor applet

Applet berikut, yang dinamakan Unsynchronized RGBColor , menunjukkan satu urutan peristiwa yang boleh mengakibatkan RGBColorobjek rosak . Benang merah dengan tidak bersalah cuba menetapkan warna menjadi merah sementara benang biru dengan bersalah cuba mengatur warna menjadi biru. Pada akhirnya, RGBColorobjek itu tidak mewakili merah atau biru tetapi warna yang tidak menentu, magenta.

Untuk sebab tertentu, penyemak imbas anda tidak akan membiarkan anda melihat applet Java yang sejuk ini.

Untuk menelusuri urutan peristiwa yang membawa kepada RGBColorobjek yang rosak , tekan butang Langkah applet. Tekan Kembali untuk menyandarkan langkah, dan Tetapkan semula untuk membuat sandaran ke awal. Semasa anda pergi, sebaris teks di bahagian bawah applet akan menerangkan apa yang berlaku pada setiap langkah.

For those of you who can't run the applet, here's a table that shows the sequence of events demonstrated by the applet:

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 setColor(255, 0, 0) 0 0 0  
red checkRGBVals(255, 0, 0); 0 0 0  
red this.r = 255; 0 0 0  
red this.g = 0; 255 0 0  
red this.b = 0; 255 0 0  
red red thread returns 255 0 0  
blue later, blue thread continues 255 0 0  
blue this.b = 255 255 0 0  
blue blue thread returns 255 0 255  
none object represents magenta 255 0 255  

As you can see from this applet and table, the RGBColor is corrupted because the thread scheduler interrupts the blue thread while the object is still in a temporarily invalid state. When the red thread comes in and paints the object red, the blue thread is only partially finished painting the object blue. When the blue thread returns to finish the job, it inadvertently corrupts the object.

Read/write conflicts

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

Penyegerakan bermaksud hanya satu utas pada satu masa yang dapat melaksanakan sedikit kod (bahagian kritikal). Oleh itu, walaupun bidang yang ingin anda selaraskan akses ke antara banyak utas, mekanisme Java untuk melakukannya sebenarnya menyelaraskan akses ke kod. Ini bermaksud bahawa hanya jika anda menjadikan data peribadi anda akan dapat mengawal akses ke data tersebut dengan mengawal akses ke kod yang memanipulasi data.