Introduksjon
Så vi vet at Java har tråder. Det kan du lese om i anmeldelsen Bedre sammen: Java og trådklassen. Del I – Tråder om henrettelse . Tråder er nødvendig for å utføre arbeid parallelt. Dette gjør det høyst sannsynlig at trådene på en eller annen måte vil samhandle med hverandre. La oss se på hvordan dette skjer og hvilke grunnleggende verktøy vi har.
Utbytte
Thread.yield() er forvirrende og brukes sjelden. Det er beskrevet på mange forskjellige måter på Internett. Inkludert noen som skriver at det er en eller annen kø med tråder, der en tråd vil gå ned basert på trådprioriteringer. Andre skriver at en tråd vil endre status fra «Kjøres» til «Kjørbar» (selv om det ikke er noe skille mellom disse statusene, dvs. Java skiller ikke mellom dem). Realiteten er at det hele er mye mindre kjent og likevel enklere på en måte.
yield()
metodens dokumentasjon. Hvis du leser den, er det tydelig atyield()
metoden gir faktisk bare en anbefaling til Java-trådplanleggeren om at denne tråden kan gis mindre utførelsestid. Men hva som faktisk skjer, altså om planleggeren handler etter anbefalingen og hva den gjør generelt, avhenger av JVMs implementering og operativsystemet. Og det kan avhenge av noen andre faktorer også. All forvirringen skyldes mest sannsynlig at multithreading har blitt tenkt nytt etter hvert som Java-språket har utviklet seg. Les mer i oversikten her: Kort introduksjon til Java Thread.yield() .
Sove
En tråd kan gå i dvale under utførelsen. Dette er den enkleste typen interaksjon med andre tråder. Operativsystemet som kjører den virtuelle Java-maskinen som kjører Java-koden vår, har sin egen trådplanlegger . Den bestemmer hvilken tråd som skal startes og når. En programmerer kan ikke samhandle med denne planleggeren direkte fra Java-kode, bare gjennom JVM. Han eller hun kan be planleggeren om å sette tråden på pause en stund, dvs. legge den i dvale. Du kan lese mer i disse artiklene: Thread.sleep() og Hvordan multithreading fungerer . Du kan også sjekke ut hvordan tråder fungerer i Windows-operativsystemer: Internaler i Windows Thread . Og la oss nå se det med egne øyne. Lagre følgende kode i en fil som heterHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Som du kan se har vi en oppgave som venter i 60 sekunder, hvoretter programmet avsluttes. Vi kompilerer ved å bruke kommandoen " javac HelloWorldApp.java
" og kjører deretter programmet med " java HelloWorldApp
". Det er best å starte programmet i et eget vindu. For eksempel, på Windows, er det slik: start java HelloWorldApp
. Vi bruker jps-kommandoen for å få PID (prosess-ID), og vi åpner listen over tråder med " jvisualvm --openpid pid
: 
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
La du merke til at vi håndterer InterruptedException
overalt? La oss forstå hvorfor.
Thread.interrupt()
Saken er at mens en tråd venter/sover kan det være noen som vil avbryte. I dette tilfellet håndterer vi enInterruptedException
. Denne mekanismen ble opprettet etter at Thread.stop()
metoden ble erklært utdatert, dvs. utdatert og uønsket. Årsaken var at når stop()
metoden ble kalt, ble tråden rett og slett «drept», noe som var svært uforutsigbart. Vi kunne ikke vite når tråden ville bli stoppet, og vi kunne ikke garantere datakonsistens. Tenk deg at du skriver data til en fil mens tråden er drept. I stedet for å drepe tråden, bestemte Javas skapere at det ville være mer logisk å fortelle den at den skulle avbrytes. Hvordan man skal svare på denne informasjonen er en sak opp til tråden selv å avgjøre. For mer informasjon, les Hvorfor er Thread.stop avviklet?på Oracles nettside. La oss se på et eksempel:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
I dette eksemplet venter vi ikke 60 sekunder. I stedet vil vi umiddelbart vise "Avbrutt". Dette er fordi vi kalte interrupt()
metoden på tråden. Denne metoden setter et internt flagg kalt "avbruddsstatus". Det vil si at hver tråd har et internt flagg som ikke er direkte tilgjengelig. Men vi har innfødte metoder for å samhandle med dette flagget. Men det er ikke den eneste måten. En tråd kan kjøre, ikke venter på noe, bare utføre handlinger. Men den kan forutse at andre vil ønske å avslutte arbeidet på et bestemt tidspunkt. For eksempel:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
I eksemplet ovenfor while
vil løkken bli utført til tråden avbrytes eksternt. Når det gjelder isInterrupted
flagget, er det viktig å vite at hvis vi fanger en InterruptedException
, blir isInterrupted-flagget tilbakestilt, og isInterrupted()
vil deretter returnere falskt. Thread-klassen har også en statisk Thread.interrupted()- metode som bare gjelder den gjeldende tråden, men denne metoden tilbakestiller flagget til false! Les mer i dette kapittelet med tittelen Trådavbrudd .
Bli med (vent til en annen tråd er ferdig)
Den enkleste typen venting er å vente på at en annen tråd skal fullføres.
public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
I dette eksemplet vil den nye tråden sove i 5 sekunder. Samtidig vil hovedtråden vente til den sovende tråden våkner og fullfører arbeidet. Hvis du ser på trådens tilstand i JVisualVM, så vil den se slik ut: 
join
er ganske enkel, fordi det bare er en metode med Java-kode som kjøres wait()
så lenge tråden den kalles på er i live. Så snart tråden dør (når den er ferdig med arbeidet), avbrytes ventetiden. Og det er hele magien med metoden join()
. Så la oss gå videre til det mest interessante.
Observere
Multithreading inkluderer konseptet med en skjerm. Ordet monitor kommer til engelsk ved hjelp av latin fra 1500-tallet og betyr "et instrument eller en enhet som brukes til å observere, sjekke eller holde en kontinuerlig oversikt over en prosess". I sammenheng med denne artikkelen vil vi prøve å dekke det grunnleggende. For alle som vil ha detaljene, vennligst dykk ned i det tilknyttede materialet. Vi begynner vår reise med Java Language Specification (JLS): 17.1. Synkronisering . Det står følgende:
lock()
eller frigjøre det med unlock()
. Deretter finner vi opplæringen på Oracle-nettstedet: Intrinsic Locks and Synchronization. Denne opplæringen sier at Javas synkronisering er bygget rundt en intern enhet kalt en indre lås eller skjermlås . Denne låsen kalles ofte ganske enkelt en " monitor ". Vi ser også igjen at hvert objekt i Java har en egen lås knyttet til seg. Du kan lese Java - Intrinsic Locks and Synchronization . Deretter vil det være viktig å forstå hvordan et objekt i Java kan assosieres med en skjerm. I Java har hvert objekt en header som lagrer interne metadata som ikke er tilgjengelige for programmereren fra koden, men som den virtuelle maskinen trenger for å fungere korrekt med objekter. Objektoverskriften inneholder et "merkeord", som ser slik ut: 
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
Her bruker den nåværende tråden (den som disse kodelinjene kjøres på) nøkkelordet synchronized
for å forsøke å bruke monitoren som er knyttet tilobject"\
variabel for å få/erverve låsen. Hvis ingen andre kjemper for skjermen (dvs. ingen andre kjører synkronisert kode ved å bruke samme objekt), kan Java prøve å utføre en optimalisering som kalles "biased locking". En relevant tag og en post om hvilken tråd som eier monitorens lås legges til merkeordet i objektoverskriften. Dette reduserer kostnadene som kreves for å låse en skjerm. Hvis skjermen tidligere var eid av en annen tråd, er slik låsing ikke nok. JVM bytter til neste type låsing: "grunnlåsing". Den bruker sammenligne-og-bytt-operasjoner (CAS). Dessuten lagrer ikke selve objektoverskriftens merkeord lenger merkeordet, men snarere en referanse til hvor det er lagret, og taggen endres slik at JVM forstår at vi bruker grunnleggende låsing. Hvis flere tråder konkurrerer (strider) om en monitor (en har skaffet seg låsen, og en andre venter på at låsen skal frigjøres), endres taggen i merkeordet, og merkeordet lagrer nå en referanse til monitoren som et objekt - en intern enhet i JVM. Som angitt i JDK Enchancement Proposal (JEP), krever denne situasjonen plass i Native Heap-området i minnet for å lagre denne enheten. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. og et sekund venter på at låsen skal frigjøres), så endres taggen i merkeordet, og merkeordet lagrer nå en referanse til monitoren som et objekt - en intern enhet i JVM. Som angitt i JDK Enchancement Proposal (JEP), krever denne situasjonen plass i Native Heap-området i minnet for å lagre denne enheten. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. og et sekund venter på at låsen skal frigjøres), så endres taggen i merkeordet, og merkeordet lagrer nå en referanse til monitoren som et objekt - en intern enhet i JVM. Som angitt i JDK Enchancement Proposal (JEP), krever denne situasjonen plass i Native Heap-området i minnet for å lagre denne enheten. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. og merkeordet lagrer nå en referanse til monitoren som et objekt - en intern enhet i JVM. Som angitt i JDK Enchancement Proposal (JEP), krever denne situasjonen plass i Native Heap-området i minnet for å lagre denne enheten. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. og merkeordet lagrer nå en referanse til monitoren som et objekt - en intern enhet i JVM. Som angitt i JDK Enchancement Proposal (JEP), krever denne situasjonen plass i Native Heap-området i minnet for å lagre denne enheten. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. Referansen til denne interne enhetens minneplassering vil bli lagret i objektoverskriftens merkeord. Dermed er en skjerm egentlig en mekanisme for å synkronisere tilgang til delte ressurser mellom flere tråder. JVM bytter mellom flere implementeringer av denne mekanismen. Så for enkelhets skyld, når vi snakker om skjermen, snakker vi faktisk om låser. 
Synkronisert (venter på lås)
Som vi så tidligere, er konseptet med en "synkronisert blokk" (eller "kritisk seksjon") nært knyttet til konseptet med en skjerm. Ta en titt på et eksempel:
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
Her sender hovedtråden først oppgaveobjektet til den nye tråden, og får deretter låsen umiddelbart og utfører en lang operasjon med den (8 sekunder). Hele denne tiden kan ikke oppgaven fortsette, fordi den ikke kan gå inn i synchronized
blokken, fordi låsen allerede er anskaffet. Hvis tråden ikke får låsen, vil den vente på skjermen. Så snart den får låsen, vil den fortsette utførelse. Når en tråd går ut av en monitor, frigjør den låsen. I JVisualVM ser det slik ut: 
th1.getState()
setningen i for-løkken vil returnere BLOCKED , fordi så lenge løkken kjører, lock
er objektets monitor okkupert av main
tråden, og th1
tråden er blokkert og kan ikke fortsette før låsen er frigjort. I tillegg til synkroniserte blokker kan en hel metode synkroniseres. For eksempel, her er en metode fra HashTable
klassen:
public synchronized int size() {
return count;
}
Denne metoden vil bli utført av bare én tråd til enhver tid. Trenger vi virkelig låsen? Ja, vi trenger det. Når det gjelder instansmetoder, fungerer "dette" objektet (gjeldende objekt) som en lås. Det er en interessant diskusjon om dette emnet her: Er det en fordel å bruke en synkronisert metode i stedet for en synkronisert blokk? . Hvis metoden er statisk, vil ikke låsen være "dette"-objektet (fordi det ikke er noe "dette"-objekt for en statisk metode), men heller et klasseobjekt (for eksempel ) Integer.class
.
Vent (venter på en skjerm). notify() og notifyAll() metoder
Thread-klassen har en annen ventemetode som er knyttet til en monitor. I motsetning tilsleep()
og join()
, kan denne metoden ikke bare kalles. Dens navn er wait()
. Metoden wait
kalles på objektet knyttet til monitoren som vi ønsker å vente på. La oss se et eksempel:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
I JVisualVM ser det slik ut: 
wait()
og notify()
er assosiert med java.lang.Object
. Det kan virke rart at trådrelaterte metoder er i Object
klassen. Men årsaken til det utfolder seg nå. Du vil huske at hvert objekt i Java har en header. Overskriften inneholder diverse husholdningsinformasjon, inkludert informasjon om monitoren, dvs. status på låsen. Husk at hvert objekt, eller forekomst av en klasse, er assosiert med en intern enhet i JVM, kalt en indre lås eller monitor. I eksemplet ovenfor indikerer koden for oppgaveobjektet at vi legger inn den synkroniserte blokken for monitoren knyttet til objektet lock
. Hvis vi lykkes med å skaffe låsen til denne skjermen, dawait()
er kalt. Tråden som utfører oppgaven vil frigjøre lock
objektets monitor, men vil gå inn i køen av tråder som venter på varsling fra objektets lock
monitor. Denne køen av tråder kalles et WAIT SET, som mer korrekt gjenspeiler formålet. Det vil si at det er mer et sett enn en kø. Tråden main
oppretter en ny tråd med oppgaveobjektet, starter den og venter i 3 sekunder. Dette gjør det høyst sannsynlig at den nye tråden vil kunne skaffe seg låsen før tråden main
, og komme inn i monitorens kø. Etter det main
går selve tråden inn i lock
objektets synkroniserte blokk og utfører trådvarsling ved hjelp av monitoren. Etter at varselet er sendt, main
frigir trådenlock
objektets monitor, og den nye tråden, som tidligere ventet på at lock
objektets monitor skulle bli utgitt, fortsetter kjøringen. Det er mulig å sende et varsel til kun én tråd ( notify()
) eller samtidig til alle tråder i køen ( notifyAll()
). Les mer her: Forskjellen mellom notify() og notifyAll() i Java . Det er viktig å merke seg at varslingsrekkefølgen avhenger av hvordan JVM er implementert. Les mer her: Hvordan løser man sult med notify and notifyAll? . Synkronisering kan utføres uten å spesifisere et objekt. Du kan gjøre dette når en hel metode er synkronisert i stedet for en enkelt kodeblokk. For statiske metoder vil for eksempel låsen være et klasseobjekt (hentet via .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
Når det gjelder bruk av låser, er begge metodene de samme. Hvis en metode ikke er statisk, vil synkronisering utføres ved å bruke gjeldende instance
, det vil si ved å bruke this
. Forresten, vi sa tidligere at du kan bruke getState()
metoden for å få statusen til en tråd. For eksempel, for en tråd i køen som venter på en monitor, vil statusen være WAITING eller TIMED_WAITING, hvis metoden wait()
spesifiserte et tidsavbrudd. 
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
Trådens livssyklus
I løpet av livet endres en tråds status. Faktisk utgjør disse endringene trådens livssyklus. Så snart en tråd er opprettet, er statusen NY. I denne tilstanden kjører ikke den nye tråden ennå, og Java-trådplanleggeren vet ennå ikke noe om den. For at trådplanleggeren skal lære om tråden, må du kalle metodenthread.start()
. Deretter vil tråden gå over til KJØRBAR-tilstanden. Internett har mange ukorrekte diagrammer som skiller mellom "Runnable" og "Running" tilstander. Men dette er en feil, fordi Java ikke skiller mellom "klar til å jobbe" (kjørbar) og "arbeid" (løper). Når en tråd er levende, men ikke aktiv (ikke kjørbar), er den i en av to tilstander:
- BLOKKERT — venter på å gå inn i en kritisk seksjon, dvs. en
synchronized
blokk. - VENTER — venter på at en annen tråd skal tilfredsstille en eller annen betingelse.
getState()
metoden. Tråder har også en isAlive()
metode, som returnerer sann hvis tråden ikke avsluttes.
LockStøtte og trådparkering
Fra og med Java 1.6 dukket det opp en interessant mekanisme kalt LockSupport .
park()
metoden returnerer umiddelbart hvis tillatelsen er tilgjengelig, og forbruker tillatelsen i prosessen. Ellers blokkerer det. Å kalle unpark
metoden gjør tillatelsen tilgjengelig dersom den ennå ikke er tilgjengelig. Det er kun 1 tillatelse. Java-dokumentasjonen for LockSupport
refererer til Semaphore
klassen. La oss se på et enkelt eksempel:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
Denne koden vil alltid vente, for nå har semaforen 0 tillatelser. Og når acquire()
det kalles inn koden (dvs. be om tillatelsen), venter tråden til den mottar tillatelsen. Siden vi venter, må vi håndtere InterruptedException
. Interessant nok får semaforen en egen trådtilstand. Hvis vi ser i JVisualVM, vil vi se at staten ikke er «Vent», men «Park». 
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
Trådens status vil være WAITING, men JVisualVM skiller mellom wait
fra synchronized
nøkkelordet og park
fra LockSupport
klassen. Hvorfor er dette LockSupport
så viktig? Vi går igjen til Java-dokumentasjonen og ser på WAITING- trådtilstanden. Som du kan se, er det bare tre måter å komme inn i det på. To av disse måtene er wait()
og join()
. Og den tredje er LockSupport
. I Java kan låser også bygges på LockSuppor
t og tilby verktøy på høyere nivå. La oss prøve å bruke en. Ta for eksempel en titt på ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
Akkurat som i de foregående eksemplene er alt enkelt her. Objektet lock
venter på at noen skal frigi den delte ressursen. Hvis vi ser i JVisualVM, vil vi se at den nye tråden vil bli parkert til tråden main
slipper låsen til den. Du kan lese mer om låser her: Java 8 StampedLocks vs. ReadWriteLocks og Synchronized and Lock API i Java. For bedre å forstå hvordan låser implementeres, er det nyttig å lese om Phaser i denne artikkelen: Veiledning til Java Phaser . Og når du snakker om forskjellige synkroniseringsenheter, må du lese DZone- artikkelen om The Java Synchronizers.
Konklusjon
I denne anmeldelsen undersøkte vi de viktigste måtene tråder samhandler på i Java. Tilleggsmateriale:- Bedre sammen: Java og Thread-klassen. Del I — Tråder om henrettelse
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION