Introduktion
Så vi ved, at Java har tråde. Det kan du læse om i anmeldelsen med titlen Better together: Java and the Thread class. Del I — Udførelsestråde . Tråde er nødvendige for at udføre arbejde parallelt. Dette gør det meget sandsynligt, at trådene på en eller anden måde vil interagere med hinanden. Lad os se på, hvordan dette sker, og hvilke grundlæggende værktøjer vi har.
Udbytte
Thread.yield() er forvirrende og bruges sjældent. Det er beskrevet på mange forskellige måder på internettet. Herunder nogle mennesker, der skriver, at der er en eller anden kø af tråde, hvor en tråd vil falde ned baseret på trådprioriteter. Andre skriver, at en tråd vil ændre sin status fra "Running" til "Runnable" (selvom der ikke er nogen forskel mellem disse statusser, dvs. Java skelner ikke mellem dem). Virkeligheden er, at det hele er meget mindre kendt og alligevel enklere i en vis forstand.
yield()
metodens dokumentation. Hvis du læser det, er det tydeligt, atyield()
metoden giver faktisk kun en anbefaling til Java-trådplanlæggeren om, at denne tråd kan få mindre eksekveringstid. Men hvad der rent faktisk sker, altså om planlæggeren handler efter anbefalingen, og hvad den gør generelt, afhænger af JVM'ens implementering og operativsystemet. Og det kan også afhænge af nogle andre faktorer. Al forvirringen skyldes højst sandsynligt, at multithreading er blevet nytænket, efterhånden som Java-sproget har udviklet sig. Læs mere i oversigten her: Kort introduktion til Java Thread.yield() .
Søvn
En tråd kan gå i dvale under dens udførelse. Dette er den nemmeste form for interaktion med andre tråde. Operativsystemet, der kører den virtuelle Java-maskine, der kører vores Java-kode, har sin egen trådplanlægger . Det bestemmer hvilken tråd der skal startes og hvornår. En programmør kan ikke interagere med denne skemalægger direkte fra Java-kode, kun gennem JVM. Han eller hun kan bede planlæggeren om at sætte tråden på pause et stykke tid, dvs. at sætte den i dvale. Du kan læse mere i disse artikler: Thread.sleep() og How Multithreading works . Du kan også tjekke, hvordan tråde fungerer i Windows-operativsystemer: Internals af Windows-tråd . Og lad os nu se det med vores egne øjne. Gem følgende kode i en fil med navnetHelloWorldApp.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 eller anden opgave, der venter i 60 sekunder, hvorefter programmet slutter. Vi kompilerer ved hjælp af kommandoen " javac HelloWorldApp.java
" og kører derefter programmet ved hjælp af " java HelloWorldApp
". Det er bedst at starte programmet i et separat vindue. For eksempel på Windows er det sådan her: start java HelloWorldApp
. Vi bruger kommandoen jps til at få PID (proces ID), og vi åbner listen over tråde med " jvisualvm --openpid pid
: 
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Har du bemærket, at vi håndterer InterruptedException
overalt? Lad os forstå hvorfor.
Thread.interrupt()
Sagen er den, at mens en tråd venter/sover, vil nogen måske afbryde. I dette tilfælde håndterer vi enInterruptedException
. Denne mekanisme blev oprettet efter Thread.stop()
metoden blev erklæret forældet, dvs. forældet og uønsket. Årsagen var, at når stop()
metoden blev kaldt, blev tråden simpelthen "slået ihjel", hvilket var meget uforudsigeligt. Vi kunne ikke vide, hvornår tråden ville blive stoppet, og vi kunne ikke garantere datakonsistens. Forestil dig, at du skriver data til en fil, mens tråden er dræbt. I stedet for at dræbe tråden besluttede Javas skabere, at det ville være mere logisk at fortælle den, at den skulle afbrydes. Hvordan man reagerer på denne information er op til tråden selv at afgøre. For flere detaljer, læs Hvorfor er Thread.stop udfaset?på Oracles hjemmeside. Lad os 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 eksempel venter vi ikke 60 sekunder. I stedet vil vi straks vise "Afbrudt". Dette er fordi vi kaldte interrupt()
metoden på tråden. Denne metode sætter et internt flag kaldet "afbrydelsesstatus". Det vil sige, at hver tråd har et internt flag, der ikke er direkte tilgængeligt. Men vi har indfødte metoder til at interagere med dette flag. Men det er ikke den eneste måde. En tråd kan køre, ikke venter på noget, blot udfører handlinger. Men den kan forvente, at andre vil ønske at afslutte sit arbejde 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 blive udført, indtil tråden afbrydes eksternt. Hvad angår isInterrupted
flaget, er det vigtigt at vide, at hvis vi fanger en InterruptedException
, bliver flaget isInterrupted nulstillet, og det isInterrupted()
vil derefter returnere falsk. Thread-klassen har også en statisk Thread.interrupted()- metode, der kun gælder for den aktuelle tråd, men denne metode nulstiller flaget til false! Læs mere i dette kapitel med titlen Trådafbrydelse .
Deltag (vent til en anden tråd er færdig)
Den enkleste form for ventetid er at vente på, at endnu en tråd slutter.
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 eksempel vil den nye tråd sove 5 sekunder. Samtidig vil hovedtråden vente, indtil den sovende tråd vågner og afslutter sit arbejde. Hvis du ser på trådens tilstand i JVisualVM, så vil det se sådan ud: 
join
er ret simpel, fordi det blot er en metode med Java-kode, der udføres, wait()
så længe tråden, den kaldes på, er i live. Så snart tråden dør (når den er færdig med sit arbejde), afbrydes ventetiden. Og det er alt det magiske ved metoden join()
. Så lad os gå videre til det mest interessante.
Overvåge
Multithreading omfatter konceptet med en skærm. Ordet monitor kommer til engelsk ved hjælp af latin fra det 16. århundrede og betyder "et instrument eller en enhed, der bruges til at observere, kontrollere eller holde en løbende registrering af en proces". I forbindelse med denne artikel vil vi forsøge at dække det grundlæggende. For alle, der ønsker detaljerne, bedes du dykke ned i de linkede materialer. Vi begynder vores rejse med Java Language Specification (JLS): 17.1. Synkronisering . Der står følgende:
lock()
eller frigive det med unlock()
. Dernæst finder vi selvstudiet på Oracle-webstedet: Intrinsic Locks and Synchronization. Denne vejledning siger, at Javas synkronisering er bygget op omkring en intern enhed kaldet en intrinsic lock eller monitor lock . Denne lås kaldes ofte blot en " monitor ". Vi ser også igen, at hvert objekt i Java har en iboende lås forbundet med sig. Du kan læse Java - Intrinsic Locks and Synchronization . Dernæst vil det være vigtigt at forstå, hvordan et objekt i Java kan associeres med en skærm. I Java har hvert objekt en header, som gemmer interne metadata, som ikke er tilgængelige for programmøren fra koden, men som den virtuelle maskine skal bruge for at arbejde korrekt med objekter. Objektoverskriften indeholder et "markeringsord", som ser sådan ud: 
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 bruger den aktuelle tråd (den, som disse kodelinjer udføres på) nøgleordet synchronized
til at forsøge at bruge den monitor, der er forbundet medobject"\
variabel for at få/erhverve låsen. Hvis ingen andre kæmper om skærmen (dvs. ingen andre kører synkroniseret kode ved hjælp af det samme objekt), så kan Java forsøge at udføre en optimering kaldet "biased locking". Et relevant tag og en post om, hvilken tråd der ejer monitorens lås, tilføjes til mærkeordet i objekthovedet. Dette reducerer det overhead, der kræves for at låse en skærm. Hvis skærmen tidligere var ejet af en anden tråd, er en sådan låsning ikke nok. JVM'en skifter til næste type låsning: "basislåsning". Den bruger sammenligne-og-byt-operationer (CAS). Desuden gemmer selve objekthovedets mærkeord ikke længere mærkeordet, men derimod en henvisning til, hvor det er gemt, og tagget ændres, så JVM'en forstår, at vi bruger grundlæggende låsning. Hvis flere tråde konkurrerer (strides) om en skærm (en har erhvervet låsen, og en anden venter på, at låsen udløses), så ændres mærket i mærkeordet, og mærkeordet gemmer nu en reference til skærmen som et objekt — en intern enhed i JVM. Som angivet i JDK Enchancement Proposal (JEP), kræver denne situation plads i hukommelsesområdet Native Heap for at gemme denne enhed. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. og et sekund venter på, at låsen udløses), så ændres mærket i mærkeordet, og mærkeordet gemmer nu en reference til monitoren som et objekt - en intern enhed i JVM. Som angivet i JDK Enchancement Proposal (JEP), kræver denne situation plads i hukommelsesområdet Native Heap for at gemme denne enhed. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. og et sekund venter på, at låsen udløses), så ændres mærket i mærkeordet, og mærkeordet gemmer nu en reference til monitoren som et objekt - en intern enhed i JVM. Som angivet i JDK Enchancement Proposal (JEP), kræver denne situation plads i hukommelsesområdet Native Heap for at gemme denne enhed. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. og mærkeordet gemmer nu en reference til monitoren som et objekt - en intern enhed i JVM. Som angivet i JDK Enchancement Proposal (JEP), kræver denne situation plads i hukommelsesområdet Native Heap for at gemme denne enhed. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. og mærkeordet gemmer nu en reference til monitoren som et objekt - en intern enhed i JVM. Som angivet i JDK Enchancement Proposal (JEP), kræver denne situation plads i hukommelsesområdet Native Heap for at gemme denne enhed. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. Referencen til denne interne enheds hukommelsesplacering vil blive gemt i objekthovedets mærkeord. Således er en skærm virkelig en mekanisme til at synkronisere adgang til delte ressourcer mellem flere tråde. JVM'en skifter mellem flere implementeringer af denne mekanisme. Så for nemheds skyld, når vi taler om skærmen, taler vi faktisk om låse. 
Synkroniseret (venter på en lås)
Som vi så tidligere, er begrebet "synkroniseret blok" (eller "kritisk sektion") tæt forbundet med begrebet en skærm. Tag et kig 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 opgaveobjektet til den nye tråd, og erhverver derefter straks låsen og udfører en lang operation med den (8 sekunder). Al denne tid kan opgaven ikke fortsætte, fordi den ikke kan komme ind i synchronized
blokken, fordi låsen allerede er erhvervet. Hvis tråden ikke kan få låsen, vil den vente på monitoren. Så snart den får låsen, vil den fortsætte med at udføre. Når en tråd forlader en skærm, udløser den låsen. I JVisualVM ser det sådan ud: 
th1.getState()
sætningen i for-løkken vil returnere BLOCKED , fordi så længe løkken kører, lock
er objektets skærm optaget af tråden main
, og th1
tråden er blokeret og kan ikke fortsætte før låsen er frigivet. Ud over synkroniserede blokke kan en hel metode synkroniseres. For eksempel, her er en metode fra HashTable
klassen:
public synchronized int size() {
return count;
}
Denne metode vil kun blive udført af én tråd på et givet tidspunkt. Har vi virkelig brug for låsen? Ja, vi har brug for det. I tilfælde af instansmetoder fungerer "dette" objektet (det nuværende objekt) som en lås. Der er en interessant diskussion om dette emne her: Er der en fordel ved at bruge en synkroniseret metode i stedet for en synkroniseret blok? . Hvis metoden er statisk, så vil låsen ikke være "dette" objektet (fordi der ikke er noget "dette" objekt for en statisk metode), men snarere et klasseobjekt (f.eks. ) Integer.class
.
Vent (venter på en skærm). notify() og notifyAll() metoder
Thread-klassen har en anden ventemetode, der er knyttet til en skærm. I modsætning tilsleep()
og join()
kan denne metode ikke blot kaldes. Dens navn er wait()
. Metoden wait
kaldes på det objekt, der er knyttet til skærmen, som vi vil vente på. Lad os 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 sådan ud: 
wait()
og notify()
er forbundet med java.lang.Object
. Det kan virke mærkeligt, at tråd-relaterede metoder er i Object
klassen. Men grunden til det udfolder sig nu. Du vil huske, at hvert objekt i Java har en header. Overskriften indeholder forskellige husholdningsoplysninger, herunder oplysninger om monitoren, dvs. låsens status. Husk, at hvert objekt eller forekomst af en klasse er knyttet til en intern enhed i JVM, kaldet en indre lås eller monitor. I eksemplet ovenfor angiver koden for opgaveobjektet, at vi indtaster den synkroniserede blok for den monitor, der er knyttet til objektet lock
. Hvis det lykkes os at anskaffe låsen til denne skærm, såwait()
Hedder. Tråden, der udfører opgaven, frigiver lock
objektets skærm, men kommer ind i køen af tråde, der venter på besked fra objektets lock
skærm. Denne kø af tråde kaldes et WAIT SET, som mere korrekt afspejler dens formål. Det vil sige, det er mere et sæt end en kø. Tråden main
opretter en ny tråd med opgaveobjektet, starter den og venter i 3 sekunder. Dette gør det meget sandsynligt, at den nye tråd vil være i stand til at erhverve låsen før tråden main
og komme ind i monitorens kø. Derefter main
går tråden selv ind i lock
objektets synkroniserede blok og udfører trådmeddelelse ved hjælp af monitoren. Efter meddelelsen er sendt, main
frigiver trådenlock
objektets skærm, og den nye tråd, som tidligere ventede på, at lock
objektets skærm blev frigivet, fortsætter eksekveringen. Det er muligt at sende en notifikation til kun én tråd ( notify()
) eller samtidigt til alle tråde i køen ( notifyAll()
). Læs mere her: Forskellen mellem notify() og notifyAll() i Java . Det er vigtigt at bemærke, at meddelelsesrækkefølgen afhænger af, hvordan JVM er implementeret. Læs mere her: Sådan løser du sult med notify og notifyAll? . Synkronisering kan udføres uden at angive et objekt. Du kan gøre dette, når en hel metode er synkroniseret i stedet for en enkelt kodeblok. For statiske metoder vil låsen f.eks. være et klasseobjekt (opnået via .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
Med hensyn til brug af låse er begge metoder de samme. Hvis en metode ikke er statisk, vil synkroniseringen blive udført ved hjælp af den aktuelle instance
, det vil sige ved hjælp af this
. Forresten sagde vi tidligere, at du kan bruge getState()
metoden til at få status for en tråd. For en tråd i køen, der f.eks. venter på en monitor, vil status være WAITING eller TIMED_WAITING, hvis metoden har wait()
angivet en timeout. 
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
Trådens livscyklus
I løbet af dens liv ændres en tråds status. Faktisk omfatter disse ændringer trådens livscyklus. Så snart en tråd er oprettet, er dens status NY. I denne tilstand kører den nye tråd endnu ikke, og Java-trådplanlæggeren ved endnu ikke noget om det. For at trådplanlæggeren kan lære om tråden, skal du kaldethread.start()
metoden. Derefter vil tråden gå over til tilstanden RUNNABLE. Internettet har masser af forkerte diagrammer, der skelner mellem "Kørbar" og "Kører" tilstande. Men dette er en fejl, fordi Java ikke skelner mellem "klar til at arbejde" (kørbar) og "arbejde" (kørende). Når en tråd er i live, men ikke aktiv (ikke kan køres), er den i en af to tilstande:
- BLOKERET — venter på at komme ind i en kritisk sektion, dvs. en
synchronized
blok. - WAITING — venter på, at en anden tråd opfylder en eller anden betingelse.
getState()
. Tråde har også en isAlive()
metode, som returnerer sand, hvis tråden ikke afsluttes.
LockSupport og trådparkering
Begyndende med Java 1.6 dukkede en interessant mekanisme kaldet LockSupport op.
park()
metoden vender straks tilbage, hvis tilladelsen er tilgængelig, og forbruge tilladelsen i processen. Ellers blokerer den. Kaldning af unpark
metoden gør tilladelsen tilgængelig, hvis den endnu ikke er tilgængelig. Der er kun 1 tilladelse. Java-dokumentationen for LockSupport
henviser til Semaphore
klassen. Lad os se på et simpelt 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 kode vil altid vente, for nu har semaforen 0 tilladelser. Og når acquire()
der kaldes i koden (dvs. anmode om tilladelsen), venter tråden indtil den modtager tilladelsen. Da vi venter, må vi håndtere InterruptedException
. Interessant nok får semaforen en separat trådtilstand. Hvis vi kigger i JVisualVM, vil vi se, at tilstanden 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 skelner mellem wait
fra synchronized
nøgleordet og park
fra LockSupport
klassen. Hvorfor er dette LockSupport
så vigtigt? Vi vender igen til Java-dokumentationen og ser på WAITING- trådtilstanden. Som du kan se, er der kun tre måder at komme ind i det på. To af disse måder er wait()
og join()
. Og den tredje er LockSupport
. I Java kan låse også bygges på LockSuppor
t og tilbyde værktøjer på højere niveau. Lad os prøve at bruge en. Tag for eksempel et kig 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();
}
}
Ligesom i de foregående eksempler er alt enkelt her. Objektet lock
venter på, at nogen frigiver den delte ressource. Hvis vi kigger i JVisualVM, vil vi se, at den nye tråd vil blive parkeret, indtil tråden main
frigiver låsen til den. Du kan læse mere om låse her: Java 8 StampedLocks vs. ReadWriteLocks og Synchronized and Lock API i Java. For bedre at forstå, hvordan låse implementeres, er det nyttigt at læse om Phaser i denne artikel: Guide til Java Phaser . Og når vi taler om forskellige synkroniseringsapparater, skal du læse DZone- artiklen om Java-synkroniseringerne.
Konklusion
I denne anmeldelse undersøgte vi de vigtigste måder, hvorpå tråde interagerer i Java. Yderligere materiale:- Bedre sammen: Java og Tråd-klassen. Del I — Udførelsestråde
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION