Hei! Vi fortsetter studiet av multithreading. I dag skal vi bli kjent med nĂžkkelordet
volatile
og yield()
metoden. La oss dykke inn :)
Det flyktige nĂžkkelordet
NÄr vi lager flertrÄdsapplikasjoner, kan vi stÞte pÄ to alvorlige problemer. For det fÞrste, nÄr en flertrÄdsapplikasjon kjÞrer, kan forskjellige trÄder cache verdiene til variabler (vi har allerede snakket om dette i leksjonen med tittelen 'Bruke flyktig' ). Du kan ha situasjonen der en trÄd endrer verdien til en variabel, men en andre trÄd ser ikke endringen, fordi den jobber med sin hurtigbufrede kopi av variabelen. Naturligvis kan konsekvensene vÊre alvorlige. Anta at det ikke er en hvilken som helst gammel variabel, men heller bankkontosaldoen din, som plutselig begynner Ä hoppe opp og ned tilfeldig :) Det hÞres ikke gÞy ut, ikke sant? For det andre, i Java, operasjoner for Ä lese og skrive alle primitive typer,long
double
, er atomare. Vel, for eksempel, hvis du endrer verdien av en int
variabel pÄ en trÄd, og pÄ en annen trÄd leser du verdien av variabelen, fÄr du enten den gamle verdien eller den nye, dvs. verdien som ble resultatet av endringen i trÄd 1. Det er ingen 'mellomverdier'. Dette fungerer imidlertid ikke med long
s og double
s. Hvorfor? PÄ grunn av stÞtte pÄ tvers av plattformer. Husker du pÄ startnivÄene at vi sa at Javas ledende prinsipp er "skriv én gang, lÞp hvor som helst"? Det betyr stÞtte pÄ tvers av plattformer. Med andre ord, en Java-applikasjon kjÞrer pÄ alle mulige forskjellige plattformer. For eksempel pÄ Windows-operativsystemer, forskjellige versjoner av Linux eller MacOS. Den vil kjÞre uten problemer pÄ dem alle. Veier inn en 64 bits,long
double
er de 'tyngste' primitivene i Java. Og visse 32-biters plattformer implementerer ganske enkelt ikke atomlesing og skriving av 64-bits variabler. Slike variabler leses og skrives i to operasjoner. FÞrst skrives de fÞrste 32 bitene til variabelen, og deretter skrives ytterligere 32 biter. Som et resultat kan det oppstÄ et problem. En trÄd skriver en 64-bits verdi til en X
variabel og gjÞr det i to operasjoner. Samtidig prÞver en andre trÄd Ä lese verdien av variabelen og gjÞr det mellom disse to operasjonene - nÄr de fÞrste 32 bitene er skrevet, men de andre 32 bitene ikke har det. Som et resultat leser den en mellomliggende, feil verdi, og vi har en feil. For eksempel, hvis vi pÄ en slik plattform prÞver Ä skrive nummeret til en 9223372036854775809 til en variabel, vil den oppta 64 biter. I binÊr form ser det slik ut: 10000000000000000000000000000000000000000000000000000000000000000000000000000001. Den fÞrste trÄden begynner Ä skrive nummeret til variabelen. FÞrst skriver den de fÞrste 32 bitene (100000000000000000000000000000000) og deretter de andre 32 bitene (000000000000000000000000000000001) Og den andre trÄden kan bli kilt mellom disse operasjonene, ved Ä lese variabelens mellomverdi (1000000000000000000000000), som er de fÞrste 32 bitene som allerede er skrevet. I desimalsystemet er dette tallet 2.147.483.648. Med andre ord, vi ville bare skrive tallet 9223372036854775809 til en variabel, men pÄ grunn av det faktum at denne operasjonen ikke er atomÊr pÄ noen plattformer, har vi det onde tallet 2,147,483,648, som kom ut av ingenting og vil ha en ukjent effekt. program. Den andre trÄden leste ganske enkelt verdien til variabelen fÞr den var ferdig skrevet, dvs. trÄden sÄ de fÞrste 32 bitene, men ikke de andre 32 bitene. Disse problemene oppsto selvfÞlgelig ikke i gÄr. Java lÞser dem med et enkelt nÞkkelord: volatile
. Hvis vi brukervolatile
nÞkkelord nÄr du erklÊrer en variabel i programmet vÄrt...
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
âŠdet betyr at:
- Den vil alltid bli lest og skrevet atomĂŠrt. Selv om det er en 64-bit
double
ellerlong
. - Java-maskinen vil ikke bufre den. SÄ du vil ikke ha en situasjon der 10 trÄder jobber med sine egne lokale kopier.
yield()-metoden
Vi har allerede gjennomgÄtt mange avThread
klassens metoder, men det er en viktig som vil vĂŠre ny for deg. Det er yield()
metoden . Og den gjĂžr akkurat det navnet tilsier! NĂ„r vi kaller yield
metoden pĂ„ en trĂ„d, snakker den faktisk til de andre trĂ„dene: 'Hei, folkens. Jeg har ikke noe sĂŠrlig hastverk med Ă„ dra noe sted, sĂ„ hvis det er viktig for noen av dere Ă„ fĂ„ prosessortid, ta det â jeg kan vente. Her er et enkelt eksempel pĂ„ hvordan dette fungerer:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
Vi oppretter og starter tre trÄder sekvensielt: Thread-0
, Thread-1
, og Thread-2
. Thread-0
starter fĂžrst og gir umiddelbart etter for de andre. SĂ„ Thread-1
er startet og gir ogsÄ. Deretter Thread-2
settes i gang, som ogsÄ gir etter. Vi har ikke flere trÄder, og etter Ä Thread-2
ha gitt sin plass sist, sier trÄdplanleggeren: 'Hmm, det er ikke flere nye trÄder. Hvem har vi i kÞen? Hvem ga sin plass fÞr Thread-2
? Det ser ut til at det var det Thread-1
. Ok, det betyr at vi lar det gÄ. Thread-1
fullfÞrer arbeidet og sÄ fortsetter trÄdplanleggeren sin koordinering: 'Ok, Thread-1
ferdig. Har vi noen andre i kÞen?'. TrÄd-0 er i kÞen: den ga sin plass rett fÞrThread-1
. Det kommer nÄ sin tur og kjÞrer til ferdigstillelse. SÄ fullfÞrer planleggeren Ä koordinere trÄdene: 'Ok, Thread-2
, du ga etter for andre trÄder, og de er alle ferdige nÄ. Du var den siste som ga etter, sÄ nÄ er det din tur. Deretter Thread-2
lÞper til ferdigstillelse. Konsullutgangen vil se slik ut: TrÄd-0 gir sin plass til andre. TrÄd-1 gir sin plass til andre. TrÄd-2 gir sin plass til andre. TrÄd-1 er fullfÞrt. TrÄd-0 er fullfÞrt. TrÄd-2 er fullfÞrt. SelvfÞlgelig kan trÄdplanleggeren starte trÄdene i en annen rekkefÞlge (for eksempel 2-1-0 i stedet for 0-1-2), men prinsippet forblir det samme.
Skjer-fĂžr-regler
Det siste vi skal berÞre i dag er konseptet " skjer fÞr ". Som du allerede vet, i Java utfÞrer trÄdplanleggeren hoveddelen av arbeidet som er involvert i Ä tildele tid og ressurser til trÄder for Ä utfÞre oppgavene deres. Du har ogsÄ gjentatte ganger sett hvordan trÄder blir utfÞrt i en tilfeldig rekkefÞlge som vanligvis er umulig Ä forutsi. Og generelt, etter den "sekvensielle" programmeringen vi gjorde tidligere, ser flertrÄdsprogrammering ut som noe tilfeldig. Du har allerede kommet til Ä tro at du kan bruke en rekke metoder for Ä kontrollere flyten til et flertrÄds program. Men multithreading i Java har en pilar til - de 4 ' skjer-fÞr' -reglene. Det er ganske enkelt Ä forstÄ disse reglene. Tenk deg at vi har to trÄder -A
ogB
. Hver av disse trÄdene kan utfÞre operasjoner 1
og 2
. I hver regel, nÄr vi sier ' A skjer-fÞr B ', mener vi at alle endringer gjort av trÄden A
fĂžr operasjon 1
og endringene som fÞlge av denne operasjonen er synlige for trÄden B
nÄr operasjonen 2
utfĂžres og deretter. 2
Hver regel garanterer at nÄr du skriver et flertrÄds program, vil visse hendelser forekomme fÞr andre 100 % av tiden, og at trÄden pÄ driftstidspunktet B
alltid vil vÊre klar over endringene som trÄden A
gjorde under operasjonen 1
. La oss vurdere dem.
Regel 1.
FrigjÞring av en mutex skjer fÞr den samme skjermen er innhentet av en annen trÄd. Jeg tror du forstÄr alt her. Hvis et objekts eller klasses mutex er hentet av én trÄd., for eksempel av trÄd , kan ikkeA
en annen trÄd (thread B
) hente den samtidig. Den mÄ vente til mutexen slippes.
Regel 2.
MetodenThread.start()
skjer fĂžr Thread.run()
. Igjen, ikke noe vanskelig her. Du vet allerede at for Ă„ begynne Ă„ kjĂžre koden inne i run()
metoden, mÄ du kalle start()
metoden pÄ trÄden. NÊrmere bestemt startmetoden, ikke run()
selve metoden! Denne regelen sikrer at verdiene til alle variabler satt fĂžr Thread.start()
kalles vil vĂŠre synlige inne i run()
metoden nÄr den begynner.
Regel 3.
Slutten avrun()
metoden skjer fĂžr returen fra join()
metoden. La oss gÄ tilbake til vÄre to trÄder: A
og B
. Vi kaller join()
metoden slik at trÄden B
garantert venter pÄ at trÄden er ferdig A
fĂžr den gjĂžr jobben sin. Dette betyr at A-objektets run()
metode garantert vil lĂžpe helt til slutten. Og alle endringer i data som skjer i run()
trÄdmetoden A
er hundre prosent garantert synlige i trÄden B
nÄr den er ferdig og venter pÄ at trÄden A
skal fullfĂžre arbeidet slik at den kan begynne sitt eget arbeid.
Regel 4.
Ă skrive til envolatile
variabel skjer fĂžr lesing fra den samme variabelen. NĂ„r vi bruker sĂžkeordet volatile
, fÄr vi faktisk alltid gjeldende verdi. Selv med et long
eller double
(vi snakket tidligere om problemer som kan skje her). Som du allerede forstÄr, er endringer som er gjort pÄ enkelte trÄder ikke alltid synlige for andre trÄder. Men det er selvfÞlgelig veldig hyppige situasjoner der slik oppfÞrsel ikke passer oss. Anta at vi tilordner en verdi til en variabel pÄ trÄden A
:
int z;
âŠ.
z = 555;
Hvis B
trÄden vÄr skulle vise verdien til z
variabelen pÄ konsollen, kan den lett vise 0, fordi den ikke vet om den tildelte verdien. Men regel 4 garanterer at hvis vi erklÊrer z
variabelen som volatile
, vil endringer i verdien pÄ én trÄd alltid vÊre synlige pÄ en annen trÄd. Hvis vi legger til ordet volatile
til forrige kode...
volatile int z;
âŠ.
z = 555;
...sÄ forhindrer vi situasjonen der trÄd B
kan vise 0. Skriving til volatile
variabler skjer fĂžr du leser fra dem.