Hei! Vi fortsetter studiet av multithreading. I dag skal vi bli kjent med nøkkelordet
Når vi kaller
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! 
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.
GO TO FULL VERSION