Introducere
Deci, știm că Java are fire. Puteți citi despre asta în recenzia intitulată Better together: Java and the Thread class. Partea I — Fire de execuție . Firele sunt necesare pentru a efectua lucrări în paralel. Acest lucru face foarte probabil ca firele să interacționeze cumva unele cu altele. Să vedem cum se întâmplă acest lucru și ce instrumente de bază avem.
Randament
Thread.yield() este derutant și rar folosit. Este descris în multe moduri diferite pe Internet. Inclusiv unii oameni care scriu că există o coadă de fire, în care un fir va coborî în funcție de prioritățile firului. Alți oameni scriu că un fir își va schimba starea din „Running” în „Runnable” (chiar dacă nu există nicio distincție între aceste stări, adică Java nu face diferența între ele). Realitatea este că totul este mult mai puțin cunoscut și totuși mai simplu într-un fel.
yield()
documentația metodei. Dacă îl citiți, este clar căyield()
De fapt, metoda oferă doar o oarecare recomandare pentru planificatorul de fire Java că acestui fir de execuție i se poate acorda mai puțin timp de execuție. Dar ceea ce se întâmplă de fapt, adică dacă planificatorul acționează pe baza recomandării și ce face în general, depinde de implementarea JVM-ului și de sistemul de operare. Și poate depinde și de alți factori. Toată confuzia se datorează cel mai probabil faptului că multithreading-ul a fost regândit pe măsură ce limbajul Java s-a dezvoltat. Citiți mai multe în prezentarea generală aici: Scurtă introducere în Java Thread.yield() .
Dormi
Un fir poate intra în somn în timpul execuției sale. Acesta este cel mai ușor tip de interacțiune cu alte fire. Sistemul de operare care rulează mașina virtuală Java care rulează codul nostru Java are propriul său programator de fire . Acesta decide ce fir să înceapă și când. Un programator nu poate interacționa cu acest planificator direct din codul Java, doar prin JVM. El sau ea poate cere programatorului să întrerupă firul pentru un timp, adică să-l pună în somn. Puteți citi mai multe în aceste articole: Thread.sleep() și Cum funcționează Multithreading . De asemenea, puteți verifica cum funcționează firele de execuție în sistemele de operare Windows: Internals of Windows Thread . Și acum să vedem cu ochii noștri. Salvați următorul cod într-un fișier numitHelloWorldApp.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();
}
}
După cum puteți vedea, avem o sarcină care așteaptă 60 de secunde, după care programul se termină. Compilăm folosind comanda " javac HelloWorldApp.java
" și apoi rulăm programul folosind " java HelloWorldApp
". Cel mai bine este să porniți programul într-o fereastră separată. De exemplu, pe Windows, este așa: start java HelloWorldApp
. Folosim comanda jps pentru a obține PID (ID-ul procesului) și deschidem lista de fire cu „ jvisualvm --openpid pid
: 
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Ai observat că ne descurcăm InterruptedException
peste tot? Să înțelegem de ce.
Thread.interrupt()
Chestia este că în timp ce un fir așteaptă/dormite, cineva poate dori să întrerupă. În acest caz, gestionăm unInterruptedException
. Acest mecanism a fost creat după ce Thread.stop()
metoda a fost declarată Deprecată, adică depășită și nedorită. Motivul a fost că atunci când stop()
metoda a fost apelată, firul a fost pur și simplu „omorât”, ceea ce era foarte imprevizibil. Nu am putut ști când va fi oprit firul și nu am putut garanta consistența datelor. Imaginați-vă că scrieți date într-un fișier în timp ce firul este oprit. În loc să distrugă firul, creatorii lui Java au decis că ar fi mai logic să-i spună că ar trebui să fie întrerupt. Cum să răspundeți la aceste informații este o chestiune care trebuie să decidă firul însuși. Pentru mai multe detalii, citiți De ce este Thread.stop depreciat?pe site-ul Oracle. Să ne uităm la un exemplu:
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();
}
În acest exemplu, nu vom aștepta 60 de secunde. În schimb, vom afișa imediat „Întrerupt”. Acest lucru se datorează faptului că am numit interrupt()
metoda pe fir. Această metodă setează un flag intern numit „stare de întrerupere”. Adică, fiecare fir are un steag intern care nu este direct accesibil. Dar avem metode native pentru a interacționa cu acest steag. Dar asta nu este singura cale. Un fir poate rula, nu așteaptă ceva, pur și simplu efectuează acțiuni. Dar poate anticipa că alții vor dori să-și încheie activitatea la un moment dat. De exemplu:
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();
}
În exemplul de mai sus, while
bucla va fi executată până când firul este întrerupt extern. În ceea ce privește isInterrupted
steagul, este important să știm că, dacă prindem un InterruptedException
, steagul isInterrupted este resetat și apoi isInterrupted()
va reveni false. Clasa Thread are, de asemenea, o metodă statică Thread.interrupted() care se aplică numai firului curent, dar această metodă resetează indicatorul la false! Citiți mai multe în acest capitol intitulat Întreruperea firului .
Alăturați-vă (Așteptați ca un alt thread să se termine)
Cel mai simplu tip de așteptare este așteptarea ca un alt fir să se termine.
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");
}
În acest exemplu, noul thread va dormi 5 secunde. În același timp, firul principal va aștepta până când firul adormit se trezește și își termină munca. Dacă te uiți la starea firului în JVisualVM, atunci va arăta astfel: 
join
este destul de simplă, deoarece este doar o metodă cu cod Java care se execută wait()
atâta timp cât thread-ul pe care este chemat este viu. De îndată ce firul moare (când se termină cu munca sa), așteptarea este întreruptă. Și asta este toată magia metodei join()
. Deci, să trecem la cel mai interesant lucru.
Monitorizați
Multithreadingul include conceptul de monitor. Cuvântul monitor vine în engleză prin limba latină din secolul al XVI-lea și înseamnă „un instrument sau dispozitiv folosit pentru observarea, verificarea sau păstrarea unei evidențe continue a unui proces”. În contextul acestui articol, vom încerca să acoperim elementele de bază. Pentru oricine dorește detalii, vă rugăm să accesați materialele legate. Ne începem călătoria cu specificația limbajului Java (JLS): 17.1. Sincronizare . Acesta spune următoarele:
lock()
sau elibera cu unlock()
. În continuare, vom găsi tutorialul pe site-ul Oracle: Intrinsic Locks and Synchronization. Acest tutorial spune că sincronizarea Java este construită în jurul unei entități interne numită blocare intrinsecă sau blocare monitor . Această blocare este adesea numită pur și simplu „ monitor ”. De asemenea, vedem din nou că fiecare obiect din Java are asociată o blocare intrinsecă. Puteți citi Java - Blocări intrinseci și sincronizare . În continuare, va fi important să înțelegem cum un obiect din Java poate fi asociat cu un monitor. În Java, fiecare obiect are un antet care stochează metadate interne care nu sunt disponibile programatorului din cod, dar de care mașina virtuală are nevoie pentru a funcționa corect cu obiectele. Antetul obiectului include un „cuvânt de marcare”, care arată astfel: 
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");
}
}
}
Aici, firul curent (cel pe care sunt executate aceste linii de cod) folosește cuvântul synchronized
cheie pentru a încerca să folosească monitorul asociat cuobject"\
variabilă pentru a obține/a dobândi blocarea. Dacă nimeni altcineva nu se luptă pentru monitor (adică nimeni altcineva nu rulează cod sincronizat folosind același obiect), atunci Java poate încerca să efectueze o optimizare numită „blocare părtinitoare”. O etichetă relevantă și o înregistrare despre firul care deține blocarea monitorului sunt adăugate la cuvântul de marcare din antetul obiectului. Acest lucru reduce supraîncărcarea necesară pentru blocarea unui monitor. Dacă monitorul a fost deținut anterior de un alt fir, atunci o astfel de blocare nu este suficientă. JVM comută la următorul tip de blocare: „blocare de bază”. Utilizează operațiuni de comparare și schimb (CAS). În plus, cuvântul de marcare al antetului obiectului în sine nu mai stochează cuvântul de marcare, ci mai degrabă o referință la locul în care este stocat, iar eticheta se schimbă, astfel încât JVM-ul să înțeleagă că folosim blocarea de bază. Dacă mai multe fire de execuție concurează (concurează) pentru un monitor (unul a obținut blocarea, iar un al doilea așteaptă ca blocarea să fie eliberată), atunci eticheta din cuvântul de marcare se schimbă, iar cuvântul de marcare stochează acum o referință la monitor ca obiect — o entitate internă a JVM. După cum se precizează în propunerea de îmbunătățire a JDK (JEP), această situație necesită spațiu în zona de memorie Native Heap pentru a stoca această entitate. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. iar o secundă așteaptă ca blocarea să fie eliberată), apoi eticheta din cuvântul de marcare se schimbă, iar cuvântul de marcare stochează acum o referință la monitor ca obiect - o entitate internă a JVM-ului. După cum se precizează în propunerea de îmbunătățire a JDK (JEP), această situație necesită spațiu în zona de memorie Native Heap pentru a stoca această entitate. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. iar o secundă așteaptă ca blocarea să fie eliberată), apoi eticheta din cuvântul de marcare se schimbă, iar cuvântul de marcare stochează acum o referință la monitor ca obiect - o entitate internă a JVM-ului. După cum se precizează în propunerea de îmbunătățire a JDK (JEP), această situație necesită spațiu în zona de memorie Native Heap pentru a stoca această entitate. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. iar cuvântul de marcare stochează acum o referință la monitor ca obiect - o entitate internă a JVM-ului. După cum se precizează în propunerea de îmbunătățire a JDK (JEP), această situație necesită spațiu în zona de memorie Native Heap pentru a stoca această entitate. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. iar cuvântul de marcare stochează acum o referință la monitor ca obiect - o entitate internă a JVM-ului. După cum se precizează în propunerea de îmbunătățire a JDK (JEP), această situație necesită spațiu în zona de memorie Native Heap pentru a stoca această entitate. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. Referința la locația de memorie a acestei entități interne va fi stocată în cuvântul de marcare al antetului obiectului. Astfel, un monitor este într-adevăr un mecanism de sincronizare a accesului la resursele partajate între mai multe fire. JVM comută între mai multe implementări ale acestui mecanism. Deci, pentru simplitate, când vorbim despre monitor, vorbim de fapt despre încuietori. 
Sincronizat (se așteaptă blocarea)
După cum am văzut mai devreme, conceptul de „bloc sincronizat” (sau „secțiune critică”) este strâns legat de conceptul de monitor. Aruncă o privire la un exemplu:
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(" ...");
}
}
Aici, firul principal trece mai întâi obiectul de sarcină noului fir, apoi dobândește imediat blocarea și efectuează o operațiune lungă cu acesta (8 secunde). În tot acest timp, sarcina nu poate continua, deoarece nu poate intra în synchronized
bloc, deoarece blocarea este deja dobândită. Dacă firul nu poate obține blocarea, va aștepta monitorul. De îndată ce primește blocarea, va continua execuția. Când un fir iese dintr-un monitor, eliberează blocarea. În JVisualVM, arată astfel: 
th1.getState()
instrucțiunea din bucla for va returna BLOCKED , deoarece atâta timp cât bucla rulează, lock
monitorul obiectului este ocupat de main
fir, iar th1
firul este blocat și nu poate continua până când blocarea este eliberată. Pe lângă blocurile sincronizate, se poate sincroniza o întreagă metodă. De exemplu, iată o metodă din HashTable
clasă:
public synchronized int size() {
return count;
}
Această metodă va fi executată de un singur fir la un moment dat. Chiar avem nevoie de lacăt? Da, avem nevoie. În cazul metodelor de instanță, obiectul „acest” (obiectul curent) acționează ca o blocare. Există o discuție interesantă pe acest subiect aici: Există un avantaj în utilizarea unei metode sincronizate în locul unui bloc sincronizat? . Dacă metoda este statică, atunci blocarea nu va fi obiectul „acest” (pentru că nu există un obiect „acest” pentru o metodă statică), ci mai degrabă un obiect Class (de exemplu, ) Integer.class
.
Așteptați (în așteptarea unui monitor). metodele notify() și notifyAll().
Clasa Thread are o altă metodă de așteptare care este asociată cu un monitor. Spre deosebiresleep()
de și join()
, această metodă nu poate fi numită pur și simplu. Numele lui este wait()
. Metoda wait
este apelată pe obiectul asociat monitorului pe care vrem să-l așteptăm. Să vedem un exemplu:
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();
}
}
În JVisualVM, arată astfel: 
wait()
și notify()
sunt asociate cu java.lang.Object
. Poate părea ciudat că metodele legate de fire sunt în Object
clasă. Dar motivul pentru asta se dezvăluie acum. Vă veți aminti că fiecare obiect din Java are un antet. Antetul conține diverse informații de menaj, inclusiv informații despre monitor, adică starea lacătului. Amintiți-vă, fiecare obiect sau instanță a unei clase este asociat cu o entitate internă în JVM, numită blocare intrinsecă sau monitor. În exemplul de mai sus, codul pentru obiectul task indică faptul că introducem blocul sincronizat pentru monitorul asociat obiectului lock
. Dacă reușim să obținem blocarea pentru acest monitor, atunciwait()
se numește. Firul care execută sarcina va elibera lock
monitorul obiectului, dar va intra în coada de fire care așteaptă notificarea de la lock
monitorul obiectului. Această coadă de fire se numește WAIT SET, care reflectă mai corect scopul său. Adică este mai mult un set decât o coadă. Firul main
de execuție creează un fir nou cu obiectul de activitate, îl pornește și așteaptă 3 secunde. Acest lucru face foarte probabil ca noul thread să poată obține blocarea înainte de thread main
și să intre în coada monitorului. După aceea, main
firul în sine intră în lock
blocul sincronizat al obiectului și efectuează notificarea firului folosind monitorul. După ce notificarea este trimisă, main
firul elibereazălock
monitorul obiectului, iar noul fir de execuție, care anterior aștepta ca lock
monitorul obiectului să fie eliberat, continuă execuția. Este posibil să trimiteți o notificare doar unui singur fir ( notify()
) sau simultan la toate firele din coadă ( notifyAll()
). Citiți mai multe aici: Diferența dintre notify() și notifyAll() în Java . Este important de reținut că ordinea de notificare depinde de modul în care este implementat JVM. Citește mai multe aici: Cum să rezolvi foamea cu notify și notifyAll? . Sincronizarea poate fi efectuată fără a specifica un obiect. Puteți face acest lucru atunci când o metodă întreagă este sincronizată, mai degrabă decât un singur bloc de cod. De exemplu, pentru metodele statice, blocarea va fi un obiect Class (obținut prin .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
În ceea ce privește utilizarea încuietorilor, ambele metode sunt aceleași. Dacă o metodă nu este statică, atunci sincronizarea va fi efectuată folosind curentul instance
, adică folosind this
. Apropo, am spus mai devreme că puteți folosi getState()
metoda pentru a obține starea unui fir. De exemplu, pentru un fir din coadă care așteaptă un monitor, starea va fi WAITING sau TIMED_WAITING, dacă metoda wait()
a specificat un timeout. 
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
Ciclul de viață al firului
Pe parcursul vieții sale, starea unui fir se schimbă. De fapt, aceste modificări includ ciclul de viață al firului. De îndată ce un fir este creat, starea acestuia este NOU. În această stare, noul fir de execuție nu rulează încă și planificatorul de fire Java nu știe încă nimic despre el. Pentru ca programatorul de fire să învețe despre fir, trebuie să apelațithread.start()
metoda. Apoi firul de execuție va trece la starea RUNNABLE. Internetul are o mulțime de diagrame incorecte care fac diferența între stările „Runnable” și „Running”. Dar aceasta este o greșeală, deoarece Java nu face diferența între „gata de lucru” (funcțional) și „funcționează” (rulează). Când un fir este activ, dar nu este activ (nu poate fi executat), acesta se află într-una din cele două stări:
- BLOCAT — așteaptă să intre într-o secțiune critică, adică un
synchronized
bloc. - AȘTEPTARE — așteptarea unui alt thread pentru a satisface o anumită condiție.
getState()
metoda. Firele au și o isAlive()
metodă, care returnează adevărat dacă firul nu este TERMINAT.
LockSupport și parcare fir
Începând cu Java 1.6, a apărut un mecanism interesant numit LockSupport .
park()
metodă revine imediat dacă permisul este disponibil, consumând permisul în proces. În caz contrar, se blochează. Apelarea unpark
metodei face permisul disponibil dacă nu este încă disponibil. Există doar 1 permis. Documentația Java pentru LockSupport
se referă la Semaphore
clasă. Să ne uităm la un exemplu simplu:
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!");
}
}
Acest cod va aștepta întotdeauna, deoarece acum semaforul are 0 permise. Iar când acquire()
este sunat în cod (adică cere permisul), threadul așteaptă până primește permisul. Din moment ce așteptăm, trebuie să ne descurcăm InterruptedException
. Interesant, semaforul primește o stare separată a firului. Dacă ne uităm în JVisualVM, vom vedea că starea nu este „Așteptați”, ci „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);
}
Starea firului de execuție va fi AȘTEPTARE, dar JVisualVM face distincție între wait
de la synchronized
cuvânt cheie și park
de la LockSupport
clasă. De ce este asta LockSupport
atât de important? Ne întoarcem din nou la documentația Java și ne uităm la starea firului AȘTEPTARE . După cum puteți vedea, există doar trei moduri de a intra în el. Două dintre aceste moduri sunt wait()
și join()
. Iar al treilea este LockSupport
. În Java, încuietorile pot fi construite și pe LockSuppor
t și oferă instrumente de nivel superior. Să încercăm să folosim unul. De exemplu, aruncați o privire la 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();
}
}
La fel ca în exemplele anterioare, aici totul este simplu. Obiectul lock
așteaptă ca cineva să elibereze resursa partajată. Dacă ne uităm în JVisualVM, vom vedea că noul thread va fi parcat până când main
thread-ul eliberează blocarea acestuia. Puteți citi mai multe despre blocări aici: Java 8 StampedLocks vs. ReadWriteLocks și Synchronized and Lock API în Java. Pentru a înțelege mai bine cum sunt implementate blocările, este util să citiți despre Phaser în acest articol: Ghid pentru Java Phaser . Și vorbind despre diverse sincronizatoare, trebuie să citiți articolul DZone despre Sincronizatoarele Java.
Concluzie
În această revizuire, am examinat principalele moduri în care firele de execuție interacționează în Java. Material suplimentar:- Mai bine împreună: Java și clasa Thread. Partea I — Fire de execuție
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION