Hej! Vi fortsätter vår studie av multithreading. Idag ska vi lära känna
volatile
nyckelordet och yield()
metoden. Låt oss dyka in :)
Det flyktiga nyckelordet
När vi skapar flertrådade applikationer kan vi stöta på två allvarliga problem. För det första, när en flertrådad applikation körs, kan olika trådar cachelagra värdena för variabler (vi har redan pratat om detta i lektionen med titeln 'Använda volatile' ) . Du kan ha situationen där en tråd ändrar värdet på en variabel, men en andra tråd ser inte ändringen, eftersom den arbetar med sin cachade kopia av variabeln. Naturligtvis kan konsekvenserna bli allvarliga. Anta att det inte är vilken gammal variabel som helst utan snarare ditt bankkontosaldo, som plötsligt börjar slumpmässigt hoppa upp och ner :) Det låter inte kul, eller hur? För det andra, i Java, operationer för att läsa och skriva alla primitiva typer,long
double
, är atomära. Tja, om du till exempel ändrar värdet på en int
variabel på en tråd, och på en annan tråd läser du värdet på variabeln, får du antingen dess gamla värde eller det nya, dvs värdet som blev resultatet av förändringen i tråd 1. Det finns inga 'mellanvärden'. Detta fungerar dock inte med long
s och double
s. Varför? På grund av plattformsoberoende stöd. Kommer du ihåg att vi på de inledande nivåerna sa att Javas vägledande princip är "skriv en gång, kör var som helst"? Det betyder plattformsoberoende stöd. Med andra ord, en Java-applikation körs på alla möjliga olika plattformar. Till exempel på Windows-operativsystem, olika versioner av Linux eller MacOS. Det kommer att fungera utan problem på dem alla. Med en vikt på 64 bitar,long
double
är de "tyngsta" primitiva i Java. Och vissa 32-bitars plattformar implementerar helt enkelt inte atomär läsning och skrivning av 64-bitars variabler. Sådana variabler läses och skrivs i två operationer. Först skrivs de första 32 bitarna till variabeln och sedan skrivs ytterligare 32 bitar. Som ett resultat kan ett problem uppstå. En tråd skriver något 64-bitars värde till en X
variabel och gör det i två operationer. Samtidigt försöker en andra tråd läsa värdet på variabeln och gör det mellan dessa två operationer - när de första 32 bitarna har skrivits, men de andra 32 bitarna inte har skrivits. Som ett resultat läser den ett mellanliggande, felaktigt värde, och vi har en bugg. Till exempel, om vi på en sådan plattform försöker skriva numret till en 9223372036854775809 till en variabel kommer den att uppta 64 bitar. I binär form ser det ut så här: 10000000000000000000000000000000000000000000000000000000000000000000000000000001 Den första tråden börjar skriva numret till variabeln. Först skriver den de första 32 bitarna (100000000000000000000000000000000) och sedan de andra 32 bitarna (0000000000000000000000000000000001) Och den andra tråden kan fastna mellan dessa operationer, läser variabelns mellanvärde (1000000000000000000000000), som är de första 32 bitarna som redan har skrivits. I decimalsystemet är detta nummer 2 147 483 648. Med andra ord, vi ville bara skriva numret 9223372036854775809 till en variabel, men på grund av det faktum att denna operation inte är atomär på vissa plattformar, har vi det onda numret 2,147,483,648, som kom från ingenstans och kommer att ha en okänd effekt. program. Den andra tråden läste helt enkelt värdet på variabeln innan den var färdigskriven, dvs tråden såg de första 32 bitarna, men inte de andra 32 bitarna. Dessa problem uppstod naturligtvis inte igår. Java löser dem med ett enda nyckelord: volatile
. Om vi användervolatile
nyckelord när du deklarerar någon variabel i vårt program...
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…det betyder att:
- Den kommer alltid att läsas och skrivas atomärt. Även om det är en 64-bitars
double
ellerlong
. - Java-maskinen cachelagrar den inte. Så du kommer inte ha en situation där 10 trådar arbetar med sina egna lokala kopior.
Metoden yield().
Vi har redan granskat många avThread
klassens metoder, men det finns en viktig som kommer att vara ny för dig. Det är yield()
metoden . Och den gör precis vad namnet antyder! När vi kallar yield
metoden på en tråd, pratar den faktiskt med de andra trådarna: 'Hej, killar. Jag har inte särskilt bråttom att åka någonstans, så om det är viktigt för någon av er att få processortid, ta det — jag kan vänta. Här är ett enkelt exempel på hur detta fungerar:
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 skapar och startar tre trådar sekventiellt: , Thread-0
, Thread-1
och Thread-2
. Thread-0
börjar först och ger genast efter för de andra. Sedan Thread-1
startas och ger också efter. Sedan Thread-2
sätts igång, vilket också ger. Vi har inga fler trådar, och efter att Thread-2
ha gett sin plats sist säger trådschemaläggaren: 'Hmm, det finns inga fler nya trådar. Vilka har vi i kön? Vem gav sin plats innan Thread-2
? Det verkar som det var Thread-1
. Okej, det betyder att vi låter det köra. Thread-1
avslutar sitt arbete och sedan fortsätter trådschemaläggaren sin koordinering: 'Okej, Thread-1
klar. Har vi någon annan i kön?'. Tråd-0 är i kön: den gav sin plats precis innanThread-1
. Det blir nu sin tur och kör till slut. Sedan avslutar schemaläggaren koordineringen av trådarna: 'Okej, , Thread-2
du gav efter för andra trådar, och de är alla klara nu. Du var den sista som gav efter, så nu är det din tur'. Går sedan Thread-2
till slut. Konsolutgången kommer att se ut så här: Tråd-0 ger sin plats till andra Tråd-1 ger sin plats till andra. Tråd-2 ger sin plats till andra. Tråd-1 har avslutats. Tråd-0 har avslutats. Tråd-2 har slutförts. Naturligtvis kan trådschemaläggaren starta trådarna i en annan ordning (till exempel 2-1-0 istället för 0-1-2), men principen förblir densamma.
Händer-före-regler
Det sista vi kommer att beröra idag är begreppet " händer före ". Som du redan vet utför trådschemaläggaren i Java huvuddelen av det arbete som är involverat i att allokera tid och resurser till trådar för att utföra sina uppgifter. Du har också upprepade gånger sett hur trådar körs i en slumpmässig ordning som vanligtvis är omöjlig att förutsäga. Och i allmänhet, efter den "sekventiella" programmeringen vi gjorde tidigare, ser flertrådsprogrammering ut som något slumpmässigt. Du har redan kommit att tro att du kan använda en mängd metoder för att kontrollera flödet av ett flertrådigt program. Men multithreading i Java har ytterligare en pelare - de fyra " händer-före "-reglerna. Att förstå dessa regler är ganska enkelt. Föreställ dig att vi har två trådar —A
ochB
. Var och en av dessa trådar kan utföra operationer 1
och 2
. I varje regel, när vi säger ' A händer-före B ', menar vi att alla ändringar som gjorts av tråden A
före operation 1
och ändringarna som är resultatet av denna operation är synliga för tråden B
när operationen 2
utförs och därefter. 2
Varje regel garanterar att när du skriver ett flertrådigt program kommer vissa händelser att inträffa före andra 100% av tiden, och att tråden vid operationstillfället B
alltid är medveten om de ändringar som tråden A
gjorde under drift 1
. Låt oss granska dem.
Regel 1.
Att släppa en mutex sker innan samma bildskärm förvärvas av en annan tråd. Jag tror att du förstår allt här. Om ett objekts eller klass mutex förvärvas av en tråd., till exempel av tråd , kanA
en annan tråd (tråd ) B
inte förvärva den samtidigt. Det måste vänta tills mutex släpps.
Regel 2.
Metoden händer tidigareThread.start()
. Återigen, inget svårt här. Du vet redan att för att börja köra koden i metoden måste du anropa metoden på tråden. Närmare bestämt startmetoden, inte själva metoden! Denna regel säkerställer att värdena för alla variabler som ställts in innan anropas kommer att vara synliga i metoden när den börjar. Thread.run()
run()
start()
run()
Thread.start()
run()
Regel 3.
Slutet pårun()
metoden sker innanjoin()
metoden återvänder . Låt oss återgå till våra två trådar: A
och B
. Vi kallar join()
metoden så att tråden B
garanterat väntar på att tråden är klar A
innan den gör sitt jobb. Det betyder att A-objektets run()
metod garanterat kommer att löpa till slutet. Och alla ändringar av data som sker i run()
trådmetoden A
är garanterat hundra procent synliga i tråden B
när den är klar och väntar på att tråden A
ska avsluta sitt arbete så att den kan börja sitt eget arbete.
Regel 4.
Att skriva till envolatile
variabel sker innan man läser från samma variabel. När vi använder volatile
sökordet får vi faktiskt alltid det aktuella värdet. Även med ett long
eller double
(vi pratade tidigare om problem som kan hända här). Som du redan förstår är ändringar som gjorts i vissa trådar inte alltid synliga för andra trådar. Men det finns förstås väldigt frekventa situationer där sådant beteende inte passar oss. Antag att vi tilldelar ett värde till en variabel på tråden A
:
int z;
….
z = 555;
Om vår B
tråd skulle visa variabelns värde z
på konsolen kan den lätt visa 0, eftersom den inte känner till det tilldelade värdet. Men regel 4 garanterar att om vi deklarerar z
variabeln som volatile
, kommer ändringar av dess värde på en tråd alltid att vara synliga på en annan tråd. Om vi lägger till ordet volatile
till föregående kod...
volatile int z;
….
z = 555;
...då förhindrar vi situationen där tråd B
kan visa 0. Att skriva till volatile
variabler sker innan man läser från dem.
GO TO FULL VERSION