Design per la sicurezza del filo

Sei mesi fa ho iniziato una serie di articoli sulla progettazione di classi e oggetti. Nella colonna Tecniche di progettazione di questo mese , continuerò quella serie esaminando i principi di progettazione che riguardano la sicurezza dei thread. Questo articolo spiega cos'è la sicurezza del thread, perché ne hai bisogno, quando ne hai bisogno e come ottenerla.

Cos'è la sicurezza dei thread?

La sicurezza dei thread significa semplicemente che i campi di un oggetto o di una classe mantengono sempre uno stato valido, come osservato da altri oggetti e classi, anche se utilizzati contemporaneamente da più thread.

Una delle prime linee guida che ho proposto in questa colonna (vedere "Progettazione dell'inizializzazione degli oggetti") è che si dovrebbero progettare classi in modo tale che gli oggetti mantengano uno stato valido, dall'inizio della loro vita alla fine. Se segui questo consiglio e crei oggetti le cui variabili di istanza sono tutte private e i cui metodi eseguono solo le transizioni di stato appropriate su quelle variabili di istanza, sei in buona forma in un ambiente a thread singolo. Ma potresti finire nei guai quando arrivano più thread.

Più thread possono causare problemi per il tuo oggetto perché spesso, mentre un metodo è in fase di esecuzione, lo stato dell'oggetto può essere temporaneamente non valido. Quando un solo thread richiama i metodi dell'oggetto, verrà mai eseguito un solo metodo alla volta e ogni metodo potrà terminare prima che venga richiamato un altro metodo. Pertanto, in un ambiente a thread singolo, a ciascun metodo verrà data la possibilità di assicurarsi che qualsiasi stato temporaneamente non valido venga modificato in uno stato valido prima che il metodo ritorni.

Una volta introdotti più thread, tuttavia, la JVM potrebbe interrompere il thread eseguendo un metodo mentre le variabili di istanza dell'oggetto sono ancora in uno stato temporaneamente non valido. La JVM potrebbe quindi dare una possibilità di esecuzione a un thread diverso e quel thread potrebbe chiamare un metodo sullo stesso oggetto. Tutto il tuo duro lavoro per rendere private le tue variabili di istanza e i tuoi metodi eseguono solo trasformazioni di stato valide non sarà sufficiente per impedire a questo secondo thread di osservare l'oggetto in uno stato non valido.

Un tale oggetto non sarebbe thread-safe, perché in un ambiente multithread, l'oggetto potrebbe essere danneggiato o essere osservato per avere uno stato non valido. Un oggetto thread-safe è quello che mantiene sempre uno stato valido, come osservato da altre classi e oggetti, anche in un ambiente multithread.

Perché preoccuparsi della sicurezza dei thread?

Ci sono due grandi ragioni per cui devi pensare alla sicurezza dei thread quando progetti classi e oggetti in Java:

  1. Il supporto per più thread è integrato nel linguaggio Java e nell'API

  2. Tutti i thread all'interno di una JVM (Java virtual machine) condividono la stessa area di heap e metodo

Poiché il multithreading è integrato in Java, è possibile che qualsiasi classe progettata alla fine possa essere utilizzata contemporaneamente da più thread. Non è necessario (e non dovresti) rendere ogni classe che progetti thread-safe, perché la thread safety non è gratuita. Ma dovresti almeno pensare alla sicurezza dei thread ogni volta che progetti una classe Java. Più avanti in questo articolo troverai una discussione sui costi della sicurezza dei thread e le linee guida su quando rendere le classi thread-safe.

Data l'architettura della JVM, devi preoccuparti solo delle variabili di istanza e di classe quando ti preoccupi della sicurezza dei thread. Poiché tutti i thread condividono lo stesso heap e l'heap è dove sono archiviate tutte le variabili di istanza, più thread possono tentare di utilizzare contemporaneamente le variabili di istanza dello stesso oggetto. Allo stesso modo, poiché tutti i thread condividono la stessa area del metodo e l'area del metodo è dove sono archiviate tutte le variabili di classe, più thread possono tentare di utilizzare le stesse variabili di classe contemporaneamente. Quando scegli di rendere una classe thread-safe, il tuo obiettivo è garantire l'integrità, in un ambiente multithread, delle variabili di istanza e di classe dichiarate in quella classe.

Non è necessario preoccuparsi dell'accesso multithread alle variabili locali, ai parametri del metodo e ai valori restituiti, poiché queste variabili risiedono nello stack Java. Nella JVM, a ogni thread viene assegnato il proprio stack Java. Nessun thread può vedere o utilizzare variabili locali, valori restituiti o parametri appartenenti a un altro thread.

Data la struttura della JVM, le variabili locali, i parametri del metodo e i valori restituiti sono intrinsecamente "thread-safe". Ma le variabili di istanza e le variabili di classe saranno thread-safe solo se si progetta la classe in modo appropriato.

RGBColor # 1: pronto per un singolo thread

Come esempio di una classe che non è thread-safe, considera la RGBColorclasse, mostrata di seguito. Le istanze di questa classe rappresentano un colore memorizzato in tre variabili di istanza private: r, g, e b. Data la classe mostrata di seguito, un RGBColoroggetto inizierebbe la sua vita in uno stato valido e subirebbe solo transizioni di stato valido, dall'inizio della sua vita alla fine, ma solo in un ambiente a thread singolo.

// In file threads / ex1 / RGBColor.java // Le istanze di questa classe NON sono thread-safe. public class RGBColor {private int r; private int g; private int b; pubblico RGBColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } / ** * restituisce il colore in un array di tre interi: R, G e B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; return 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 (); }}}

Poiché le tre variabili di istanza, ints r, ge b, sono private, l'unico modo in cui altre classi e oggetti possono accedere o influenzare i valori di queste variabili è tramite RGBColoril costruttore e i metodi di. Il design del costruttore e dei metodi garantisce che:

  1. RGBColorIl costruttore di darà sempre alle variabili i valori iniziali corretti

  2. Metodi setColor()e invert()eseguiranno sempre trasformazioni di stato valide su queste variabili

  3. Il metodo getColor()restituirà sempre una visualizzazione valida di queste variabili

Si noti che se vengono passati dati errati al costruttore o al setColor()metodo, verranno completati all'improvviso con un file InvalidArgumentException. Il checkRGBVals()metodo, che genera questa eccezione, in effetti definisce cosa significa per un RGBColoroggetto sia valida: i valori di tutte e tre le variabili, r, g, e b, deve essere compreso tra 0 e 255, estremi inclusi. Inoltre, per essere valido, il colore rappresentato da queste variabili deve essere il colore più recente passato al costruttore o al setColor()metodo, oppure prodotto dal invert()metodo.

If, in a single-threaded environment, you invoke setColor() and pass in blue, the RGBColor object will be blue when setColor() returns. If you then invoke getColor() on the same object, you'll get blue. In a single-threaded society, instances of this RGBColor class are well-behaved.

Throwing a concurrent wrench into the works

Unfortunately, this happy picture of a well-behaved RGBColor object can turn scary when other threads enter the picture. In a multithreaded environment, instances of the RGBColor class defined above are susceptible to two kinds of bad behavior: write/write conflicts and read/write conflicts.

Write/write conflicts

Imagine you have two threads, one thread named "red" and another named "blue." Both threads are trying to set the color of the same RGBColor object: The red thread is trying to set the color to red; the blue thread is trying to set the color to blue.

Both of these threads are trying to write to the same object's instance variables concurrently. If the thread scheduler interleaves these two threads in just the right way, the two threads will inadvertently interfere with each other, yielding a write/write conflict. In the process, the two threads will corrupt the object's state.

The Unsynchronized RGBColor applet

La seguente applet, denominata RGBColor non sincronizzata , mostra una sequenza di eventi che potrebbe causare un RGBColoroggetto danneggiato . Il filo rosso sta tentando innocentemente di impostare il colore sul rosso mentre il filo blu sta tentando innocentemente di impostare il colore sul blu. Alla fine, l' RGBColoroggetto non rappresenta né il rosso né il blu ma il colore inquietante, il magenta.

Per qualche ragione, il tuo browser non ti permetterà di vedere in questo modo una fantastica applet Java.

Per scorrere la sequenza di eventi che portano a un RGBColoroggetto danneggiato , premere il pulsante Step dell'applet. Premere Indietro per tornare indietro di un passaggio e Reimposta per tornare all'inizio. Mentre procedi, una riga di testo nella parte inferiore dell'applet spiegherà cosa sta succedendo durante ogni passaggio.

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

La sincronizzazione significa che solo un thread alla volta sarà in grado di eseguire un po 'di codice (una sezione critica). Quindi, anche se si tratta di campi a cui si desidera coordinare l'accesso tra più thread, il meccanismo di Java per farlo coordina effettivamente l'accesso al codice. Ciò significa che solo se rendi privati ​​i dati sarai in grado di controllare l'accesso a quei dati controllando l'accesso al codice che manipola i dati.