Design för trådsäkerhet

För sex månader sedan började jag en serie artiklar om att designa klasser och objekt. I den här månadens kolumn för designtekniker fortsätter jag den serien genom att titta på designprinciper som gäller trådsäkerhet. Den här artikeln berättar vad trådsäkerhet är, varför du behöver det, när du behöver det och hur du ska få det.

Vad är trådsäkerhet?

Trådsäkerhet betyder helt enkelt att fälten i ett objekt eller klass alltid upprätthåller ett giltigt tillstånd, vilket observeras av andra objekt och klasser, även när de används samtidigt av flera trådar.

En av de första riktlinjerna som jag föreslog i den här kolumnen (se "Designa objektinitialisering") är att du ska utforma klasser så att objekt bibehåller ett giltigt tillstånd, från början av deras livstid till slutet. Om du följer detta råd och skapar objekt vars instansvariabler alla är privata och vars metoder bara gör korrekta tillståndsövergångar på dessa instansvariabler, är du i god form i en miljö med en tråd. Men du kan komma i trubbel när fler trådar kommer.

Flera trådar kan stava problem för ditt objekt eftersom ofta, medan en metod håller på att köras, kan objektets tillstånd vara tillfälligt ogiltigt. När bara en tråd påkallar objektets metoder kommer bara en metod åt gången att köras och varje metod får slutföras innan en annan metod åberopas. Således, i en engängad miljö, kommer varje metod att ges en chans att se till att alla tillfälligt ogiltiga tillstånd ändras till ett giltigt tillstånd innan metoden återvänder.

När du väl har introducerat flera trådar kan JVM dock avbryta tråden som kör en metod medan objektets instansvariabler fortfarande är i ett tillfälligt ogiltigt tillstånd. JVM kan sedan ge en annan tråd en chans att köra, och den tråden kan kalla en metod för samma objekt. Allt ditt hårda arbete för att göra dina instansvariabler privata och dina metoder utför endast giltiga tillståndstransformationer räcker inte för att hindra denna andra tråd från att observera objektet i ogiltigt tillstånd.

Ett sådant objekt skulle inte vara trådsäkert, för i en multitrådad miljö kan objektet bli skadat eller observeras ha ett ogiltigt tillstånd. Ett trådsäkert objekt är ett objekt som alltid upprätthåller ett giltigt tillstånd, vilket observeras av andra klasser och objekt, även i en multitrådad miljö.

Varför oroa dig för trådsäkerhet?

Det finns två stora anledningar till att du behöver tänka på trådsäkerhet när du utformar klasser och objekt i Java:

  1. Stöd för flera trådar är inbyggt i Java-språket och API

  2. Alla trådar i en Java-virtuell maskin (JVM) delar samma heap- och metodområde

Eftersom multithreading är inbyggt i Java är det möjligt att alla klasser du utformar så småningom kan användas samtidigt av flera trådar. Du behöver inte (och borde inte) göra varje klass du designar trådsäker, för trådsäkerhet kommer inte gratis. Men du bör åtminstone tänka på trådsäkerhet varje gång du utformar en Java-klass. Du hittar en diskussion om kostnaderna för trådsäkerhet och riktlinjer för när du ska göra klasser trådsäkra senare i den här artikeln.

Med tanke på JVM: s arkitektur behöver du bara vara bekymrad över instans- och klassvariabler när du oroar dig för trådsäkerhet. Eftersom alla trådar delar samma hög och högen är där alla instansvariabler lagras kan flera trådar försöka använda samma objekts instansvariabler samtidigt. På samma sätt, eftersom alla trådar delar samma metodområde och metodområdet är där alla klassvariabler lagras, kan flera trådar försöka använda samma klassvariabler samtidigt. När du väljer att göra en klasstrådsäker är ditt mål att garantera integriteten - i en multitrådad miljö - av instanser och klassvariabler som deklarerats i den klassen.

Du behöver inte oroa dig för flertrådad åtkomst till lokala variabler, metodparametrar och returvärden, eftersom dessa variabler finns på Java-stacken. I JVM tilldelas varje tråd sin egen Java-stack. Ingen tråd kan se eller använda lokala variabler, returvärden eller parametrar som tillhör en annan tråd.

Med tanke på strukturen för JVM är lokala variabler, metodparametrar och returvärden i sig "trådsäkra." Men instansvariabler och klassvariabler är endast trådsäkra om du utformar din klass på lämpligt sätt.

RGBColor # 1: Redo för en enda tråd

Tänk på klassen som visas nedan som ett exempel på en klass som inte är trådsäker RGBColor. Instanser av denna klass representerar en färg som lagrats i tre privata instansvariabler: r, goch b. Med tanke på klassen som visas nedan, skulle ett RGBColorobjekt börja sitt liv i ett giltigt tillstånd och endast uppleva giltiga tillståndsövergångar, från början av sitt liv till slutet - men bara i en enda tråd.

// I filtrådar / ex1 / RGBColor.java // Instanser av denna klass är INTE trådsäkra. offentlig klass RGBColor {privat int r; privat int g; privat int b; offentlig 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; } / ** * returnerar färg i en matris med tre stycken: R, G och 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; } privat statisk ogiltigkontrollRGBVals (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {kasta nytt IllegalArgumentException (); }}}

Eftersom de tre instansvariablerna, ints r, goch b, är privata, är det enda sättet som andra klasser och objekt kan komma åt eller påverka värdena för dessa variabler via RGBColorkonstruktören och metoderna. Konstruktörens utformning och metoder garanterar att:

  1. RGBColors konstruktör kommer alltid att ge variablerna rätt initialvärden

  2. Metoder setColor()och invert()kommer alltid att utföra giltiga tillståndstransformationer på dessa variabler

  3. Metoden getColor()returnerar alltid en giltig vy av dessa variabler

Observera att om dåliga data skickas till konstruktören eller setColor()metoden kommer de att slutföra plötsligt med en InvalidArgumentException. Den checkRGBVals()metod, som kastar detta undantag, i själva verket definierar vad det innebär för ett RGBColorobjekt för att vara giltigt: värdena för alla tre variablerna, r, g, och b, måste vara mellan 0 och 255, inklusive. För att vara giltig måste dessutom färgen som representeras av dessa variabler vara den senaste färgen antingen skickad till konstruktören eller setColor()metoden eller framställs av invert()metoden.

Om du i en entrådig miljö anropar setColor()och skickar i blått blir RGBColorobjektet blått när det setColor()returneras. Om du sedan åberopar getColor()samma objekt får du blått. I ett samhälle med en tråd, är fall av denna RGBColorklass välskötta.

Kasta en samtidigt skiftnyckel i verk

Tyvärr kan den här glada bilden av ett välskött RGBColorföremål bli skrämmande när andra trådar kommer in i bilden. I en flertrådad miljö är förekomster av den RGBColorklass som definierats ovan mottagliga för två typer av dåligt beteende: skriv / skriv konflikter och läs / skriv konflikter.

Skriv / skriv konflikter

Tänk dig att du har två trådar, en tråd som heter "röd" och en annan som heter "blå". Båda trådarna försöker ställa in färgen på samma RGBColorobjekt: Den röda tråden försöker ställa in färgen till röd; den blå tråden försöker ställa in färgen till blå.

Båda dessa trådar försöker skriva till samma objekts instansvariabler samtidigt. Om trådschemaläggaren sammanfogar dessa två trådar på rätt sätt kommer de två trådarna oavsiktligt att störa varandra och ge en skriv / skriv-konflikt. Under processen kommer de två trådarna att skada objektets tillstånd.

Den OsynkroniseradRGBColor applet

Följande applet, med namnet Unsynchronized RGBColor , visar en sekvens av händelser som kan resultera i ett korrupt RGBColorobjekt. Den röda tråden försöker oskyldigt ställa in färgen till röd medan den blå tråden oskyldigt försöker ställa in färgen till blå. I slutändan RGBColorrepresenterar objektet varken rött eller blått utan den oroande färgen magenta.

Av någon anledning låter din webbläsare dig inte se det här coola Java-appletet.

Om du vill gå igenom sekvensen av händelser som leder till ett skadat RGBColorobjekt trycker du på applets steg-knapp. Tryck på Tillbaka för att säkerhetskopiera ett steg och Återställ för att säkerhetskopiera till början. När du går kommer en textrad längst ner i appleten att förklara vad som händer under varje steg.

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

Synkronisering innebär att endast en tråd åt gången kommer att kunna utföra lite kod (ett kritiskt avsnitt). Så även om det är fält du vill samordna åtkomst till mellan flera trådar, koordinerar Java: s mekanism för att göra det faktiskt åtkomst till kod. Detta innebär att endast om du gör data privata kommer du att kunna kontrollera åtkomst till dessa data genom att kontrollera åtkomst till koden som manipulerar data.