Hvad er trådsikkerhed og hvordan opnås det?

1. Oversigt

Java understøtter multithreading ud af kassen. Dette betyder, at ved at køre bytecode samtidigt i separate arbejdstråde, er JVM i stand til at forbedre applikationsydelsen.

Selvom multithreading er en stærk funktion, har den en pris. I flertrådede miljøer er vi nødt til at skrive implementeringer på en trådsikker måde. Dette betyder, at forskellige tråde kan få adgang til de samme ressourcer uden at udsætte fejlagtig adfærd eller producere uforudsigelige resultater. Denne programmeringsmetode er kendt som “tråd-sikkerhed”.

I denne vejledning ser vi på forskellige tilgange til at opnå det.

2. Statsløse implementeringer

I de fleste tilfælde er fejl i multitrådede applikationer resultatet af forkert deling af tilstand mellem flere tråde.

Derfor er den første tilgang, vi vil se på, at opnå trådsikkerhed ved hjælp af statsløse implementeringer.

For bedre at forstå denne tilgang, lad os overveje en simpel brugsklasse med en statisk metode, der beregner faktornummeret for et tal:

offentlig klasse MathUtils {offentlig statisk BigInteger-faktor (int-nummer) {BigInteger f = nyt BigInteger ("1"); for (int i = 2; i <= antal; i ++) {f = f.multiply (BigInteger.valueOf (i)); } returner f; }} 

Det fabrik () metoden er en statsløs deterministisk funktion. Med et specifikt input producerer det altid det samme output.

Metoden hverken er afhængig af ekstern tilstand eller opretholder overhovedet tilstand. Derfor anses det for at være trådsikkert og kan sikkert kaldes af flere tråde på samme tid.

Alle tråde kan trygt ringe til fabrik () metode og får det forventede resultat uden at forstyrre hinanden og uden at ændre det output, som metoden genererer til andre tråde.

Derfor, statsløse implementeringer er den enkleste måde at opnå trådsikkerhed på.

3. Uforanderlige implementeringer

Hvis vi har brug for at dele tilstand mellem forskellige tråde, kan vi oprette trådsikre klasser ved at gøre dem uforanderlige.

Uforanderlighed er et stærkt sprog-agnostisk koncept, og det er ret nemt at opnå i Java.

For at sige det enkelt, en klasseinstans er uforanderlig, når dens interne tilstand ikke kan ændres, efter at den er konstrueret.

Den nemmeste måde at oprette en uforanderlig klasse på Java er ved at erklære alle felterne privat og endelig og ikke leverer settere:

offentlig klasse MessageService {privat endelig streng besked; public MessageService (streng besked) {this.message = meddelelse; } // standard getter}

EN MessageService objekt er effektivt uforanderligt, da dets tilstand ikke kan ændre sig efter dets konstruktion. Derfor er det trådsikkert.

Desuden, hvis MessageService var faktisk ændrede, men flere tråde har kun skrivebeskyttet adgang til det, det er også trådsikkert.

Dermed, uforanderlighed er bare en anden måde at opnå trådsikkerhed på.

4. Tråd-lokale felter

I objektorienteret programmering (OOP) skal objekter faktisk opretholde tilstand gennem felter og implementere adfærd gennem en eller flere metoder.

Hvis vi faktisk har brug for at opretholde staten, Vi kan oprette trådsikre klasser, der ikke deler tilstand mellem tråde ved at gøre deres felter trådlokale.

Vi kan nemt oprette klasser, hvis felter er trådlokale ved blot at definere private felter i Tråd klasser.

Vi kunne f.eks. Definere en Tråd klasse, der gemmer en array af heltal:

public class ThreadA udvider Thread {private final Listnumre = Arrays.asList (1, 2, 3, 4, 5, 6); @Override public void run () {numbers.forEach (System.out :: println); }}

Mens en anden måske holder en array af strenge:

public class ThreadB udvider Thread {private final Liste bogstaver = Arrays.asList ("a", "b", "c", "d", "e", "f"); @Override public void run () {letters.forEach (System.out :: println); }}

I begge implementeringer har klasserne deres egen tilstand, men den deles ikke med andre tråde. Således er klasserne trådsikre.

På samme måde kan vi oprette tråd-lokale felter ved at tildele Trådlokal forekomster til et felt.

Lad os for eksempel overveje følgende StateHolder klasse:

offentlig klasse StateHolder {privat endelig strengstat; // standardkonstruktører / getter}

Vi kan nemt gøre det til en tråd-lokal variabel som følger:

offentlig klasse ThreadState {offentlig statisk endelig ThreadLocal statePerThread = ny ThreadLocal () {@ Override beskyttet StateHolder initialValue () {returner ny StateHolder ("aktiv"); }}; offentlig statisk StateHolder getState () {return statePerThread.get (); }}

Tråd-lokale felter ligner stort set normale klassefelter, bortset fra at hver tråd, der får adgang til dem via en setter / getter, får en uafhængigt initialiseret kopi af feltet, så hver tråd har sin egen tilstand.

5. Synkroniserede samlinger

Vi kan nemt oprette trådsikre samlinger ved hjælp af det sæt synkroniseringsindpakninger, der er inkluderet i samlingens rammer.

Vi kan for eksempel bruge en af ​​disse synkroniseringsindpakninger til at oprette en trådsikker samling:

Collection syncCollection = Collections.synchronizedCollection (ny ArrayList ()); Tråd tråd1 = ny tråd (() -> syncCollection.addAll (Arrays.asList (1, 2, 3, 4, 5, 6))); Tråd tråd2 = ny tråd (() -> syncCollection.addAll (Arrays.asList (7, 8, 9, 10, 11, 12))); thread1.start (); thread2.start (); 

Lad os huske på, at synkroniserede samlinger bruger indre låsning i hver metode (vi vil se på indre låsning senere).

Dette betyder, at metoderne kun kan tilgås med en tråd ad gangen, mens andre tråde blokeres, indtil metoden låses op af den første tråd.

Således har synkronisering en straf i ydeevne på grund af den underliggende logik med synkroniseret adgang.

6. Samtidige samlinger

Alternativt til synkroniserede samlinger kan vi bruge samtidige samlinger til at oprette trådsikre samlinger.

Java leverer java.util.concurrent pakke, der indeholder flere samtidige samlinger, f.eks ConcurrentHashMap:

Kort concurrentMap = ny ConcurrentHashMap (); concurrentMap.put ("1", "one"); concurrentMap.put ("2", "to"); concurrentMap.put ("3", "tre"); 

I modsætning til deres synkroniserede kolleger, samtidige samlinger opnår trådsikkerhed ved at opdele deres data i segmenter. I en ConcurrentHashMapfor eksempel kan flere tråde erhverve låse på forskellige kortsegmenter, så flere tråde kan få adgang til Kort på samme tid.

Samtidige samlinger ermeget mere performant end synkroniserede samlingerpå grund af de iboende fordele ved samtidig trådadgang.

Det er værd at nævne det synkroniserede og samtidige samlinger gør kun selve samlingen trådsikker og ikke indholdet.

7. Atomiske objekter

Det er også muligt at opnå trådsikkerhed ved hjælp af det sæt atomklasser, som Java giver, inklusive AtomicInteger, AtomicLong, AtomicBooleanog AtomicReference.

Atomklasser giver os mulighed for at udføre atomoperationer, som er trådsikre, uden at bruge synkronisering. En atomoperation udføres i en enkelt maskinniveauoperation.

Lad os se på følgende for at forstå det problem, dette løser Tæller klasse:

offentlig klassetæller {privat int-tæller = 0; public void incrementCounter () {counter + = 1; } public int getCounter () {return counter; }}

Lad os antage, at der i løbet af løbet er to tråde adgang til incrementCounter () metode på samme tid.

I teorien er den endelige værdi af tæller felt vil være 2. Men vi kan bare ikke være sikre på resultatet, fordi trådene udfører den samme kodeblok på samme tid, og inkrementering er ikke atomisk.

Lad os oprette en trådsikker implementering af Tæller klasse ved hjælp af en AtomicInteger objekt:

offentlig klasse AtomicCounter {private final AtomicInteger counter = new AtomicInteger (); public void incrementCounter () {counter.incrementAndGet (); } public int getCounter () {return counter.get (); }}

Dette er trådsikkert, fordi, mens inkrementering, ++, tager mere end en operation, incrementAndGet er atomisk.

8. Synkroniserede metoder

Mens de tidligere tilgange er meget gode til samlinger og primitiver, har vi til tider brug for større kontrol end det.

Så en anden almindelig tilgang, som vi kan bruge til at opnå trådsikkerhed, er implementering af synkroniserede metoder.

Kort fortalt, kun en tråd kan få adgang til en synkroniseret metode ad gangen, mens den blokerer adgang til denne metode fra andre tråde. Andre tråde forbliver blokeret, indtil den første tråd er færdig, eller metoden kaster en undtagelse.

Vi kan oprette en trådsikker version af incrementCounter () på en anden måde ved at gøre det til en synkroniseret metode:

offentlig synkroniseret ugyldig incrementCounter () {counter + = 1; }

Vi har oprettet en synkroniseret metode ved at forud for metodesignaturen med synkroniseret nøgleord.

Da en tråd ad gangen kan få adgang til en synkroniseret metode, vil en tråd udføre incrementCounter () metode, og igen vil andre gøre det samme. Ingen overlappende udførelse vil overhovedet forekomme.

Synkroniserede metoder er afhængige af brugen af ​​"indre låse" eller "skærmlåse". En indre lås er en implicit intern enhed tilknyttet en bestemt klasseinstans.

I en multitrådet sammenhæng, udtrykket overvåge er kun en henvisning til den rolle, som låsen udfører på det tilknyttede objekt, da den tvinger eksklusiv adgang til et sæt specificerede metoder eller udsagn.

Når en tråd kalder en synkroniseret metode, får den den indre lås. Når tråden er færdig med at udføre metoden, frigiver den låsen, hvilket giver andre tråde mulighed for at erhverve låsen og få adgang til metoden.

Vi kan implementere synkronisering i eksempelvis metoder, statiske metoder og udsagn (synkroniserede udsagn).

9. Synkroniserede udsagn

Nogle gange kan synkronisering af en hel metode være overkill, hvis vi bare har brug for at gøre et segment af metoden trådsikker.

Lad os reflektere for at eksemplificere denne brugssag incrementCounter () metode:

public void incrementCounter () {// yderligere usynkroniserede operationer synkroniseret (dette) {counter + = 1; }}

Eksemplet er trivielt, men det viser, hvordan man opretter en synkroniseret erklæring. Under forudsætning af at metoden nu udfører et par yderligere operationer, som ikke kræver synkronisering, synkroniserede vi kun det relevante tilstandsændrende afsnit ved at indpakke det i en synkroniseret blok.

I modsætning til synkroniserede metoder skal synkroniserede udsagn specificere det objekt, der giver den indre lås, normalt det her reference.

Synkronisering er dyr, så med denne mulighed er vi i stand til kun at synkronisere de relevante dele af en metode.

9.1. Andre objekter som en lås

Vi kan forbedre den trådsikre implementering af Tæller klasse ved at udnytte et andet objekt som en monitorlås i stedet for det her.

Dette giver ikke kun koordineret adgang til en delt ressource i et multitrådet miljø, men det bruger også en ekstern enhed til at håndhæve eksklusiv adgang til ressourcen:

offentlig klasse ObjectLockCounter {privat int tæller = 0; privat endelig Objektlås = nyt objekt (); public void incrementCounter () {synkroniseret (lås) {counter + = 1; }} // standard getter}

Vi bruger en slette Objekt eksempel for at håndhæve gensidig udstødelse. Denne implementering er lidt bedre, da den fremmer sikkerhed på låseniveau.

Ved brug det her til indre låsning, en angriber kan forårsage et dødvande ved at erhverve den indre lås og udløse en Denial of Service (DoS) -tilstand.

Tværtimod, når du bruger andre objekter, at den private enhed ikke er tilgængelig udefra. Dette gør det sværere for en angriber at erhverve låsen og forårsage en blokering.

9.2. Advarsler

Selvom vi kan bruge ethvert Java-objekt som en indre lås, bør vi undgå at bruge Strenge til låsning:

offentlig klasse klasse 1 {privat statisk endelig streng LOCK = "Lås"; // bruger LÅSEN som den indre lås} offentlig klasse Class2 {privat statisk endelig streng LÅS = "Lås"; // bruger LÅSEN som den indre lås}

Ved første øjekast ser det ud til, at disse to klasser bruger to forskellige objekter som deres lås. Imidlertid, på grund af strenginterning kan disse to "Lock" -værdier faktisk henvise til det samme objekt i strengpoolen. Det vil sige Klasse1 og Klasse 2 deler den samme lås!

Dette kan igen medføre uventet adfærd i samtidige sammenhænge.

I tillæg til Strenge, vi bør undgå at bruge genstande, der kan caches eller genanvendes, som iboende låse. F.eks Integer.valueOf () metode cacher små tal. Derfor ringer Integer.valueOf (1) returnerer det samme objekt, selv i forskellige klasser.

10. Flygtige felter

Synkroniserede metoder og blokke er nyttige til løsning af problemer med variabel synlighed blandt tråde. Alligevel kan værdierne for almindelige klassefelter blive cachelagret af CPU'en. Derfor kan opdateringer til et bestemt felt, selvom de er synkroniserede, muligvis ikke synlige for andre tråde.

For at forhindre denne situation kan vi bruge flygtige klasse felter:

offentlig klassetæller {privat flygtig tæller; // standardkonstruktører / getter}

Med flygtige nøgleord, vi instruerer JVM og compileren om at gemme tæller variabel i hovedhukommelsen. På den måde sørger vi for, at hver gang JVM læser værdien af tæller variabel, vil den faktisk læse den fra hovedhukommelsen i stedet for fra CPU-cachen. Ligeledes hver gang JVM skriver til tæller variabel, vil værdien blive skrevet til hovedhukommelsen.

I øvrigt, brugen af ​​en flygtige variabel sikrer, at alle variabler, der er synlige for en given tråd, også læses fra hovedhukommelsen.

Lad os overveje følgende eksempel:

offentlig klasse bruger {privat strengnavn; privat flygtig alder; // standard konstruktører / getters}

I dette tilfælde skriver JVM hver gang alderflygtige variabel til hovedhukommelsen, skriver den den ikke-flygtige navn variabel til hovedhukommelsen. Dette sikrer, at de nyeste værdier for begge variabler er gemt i hovedhukommelsen, så deraf følgende opdateringer til variablerne automatisk vil være synlige for andre tråde.

Tilsvarende, hvis en tråd læser værdien af ​​en flygtige variabel, læses alle variabler, der er synlige for tråden, også fra hovedhukommelsen.

Denne udvidede garanti for, at flygtige variabler giver er kendt som den fulde garanti for ustabil synlighed.

11. Genindlåsning

Java giver et forbedret sæt af Låse implementeringer, hvis opførsel er lidt mere sofistikeret end de iboende låse, der er diskuteret ovenfor.

Med indre låse er modellen til låseanskaffelse ret stiv: en tråd erhverver låsen, udfører derefter en metode eller kodeblok og frigiver endelig låsen, så andre tråde kan erhverve den og få adgang til metoden.

Der er ingen underliggende mekanisme, der kontrollerer trådene i kø og giver prioritet adgang til de længste ventende tråde.

ReentrantLock tilfælde giver os mulighed for at gøre netop det, derfor forhindrer trådene i kø i at lide nogle typer ressource sult:

offentlig klasse ReentrantLockCounter {privat int-tæller; privat endelig ReentrantLock reLock = ny ReentrantLock (sand); public void incrementCounter () {reLock.lock (); prøv {counter + = 1; } endelig {reLock.unlock (); }} // standardkonstruktører / getter}

Det ReentrantLock konstruktør tager en valgfri retfærdighedboolsk parameter. Når indstillet til rigtigt, og flere tråde forsøger at skaffe en lås, JVM prioriterer den længste ventetråd og giver adgang til låsen.

12. Læs / skriv lås

En anden stærk mekanisme, som vi kan bruge til at opnå trådsikkerhed, er brugen af ReadWriteLock implementeringer.

EN ReadWriteLock lock bruger faktisk et par tilknyttede låse, en til skrivebeskyttet handling og en anden til skriveoperationer.

Som resultat, det er muligt at have mange tråde, der læser en ressource, så længe der ikke er nogen tråd, der skriver til den. Desuden vil trådskrivning til ressourcen forhindre andre tråde i at læse den.

Vi kan bruge en ReadWriteLock lås som følger:

offentlig klasse ReentrantReadWriteLockCounter {privat int-tæller; privat endelig ReentrantReadWriteLock rwLock = ny ReentrantReadWriteLock (); privat endelig Lås readLock = rwLock.readLock (); privat endelig Lås skriveLås = rwLock.writeLock (); public void incrementCounter () {writeLock.lock (); prøv {counter + = 1; } endelig {writeLock.unlock (); }} offentlig int getCounter () {readLock.lock (); prøv {retur tæller; } endelig {readLock.unlock (); }} // standardkonstruktører} 

13. Konklusion

I denne artikel vi lærte hvad trådsikkerhed er i Java og kiggede grundigt på forskellige tilgange til at opnå det.

Som sædvanligt er alle kodeeksempler vist i denne artikel tilgængelige på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found