CodeGym /Blog Java /Aleatoriu /Mai bine împreună: Java și clasa Thread. Partea a II-a — ...
John Squirrels
Nivel
San Francisco

Mai bine împreună: Java și clasa Thread. Partea a II-a — Sincronizarea

Publicat în grup

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. Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 1

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. Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 2Există o eroare ( JDK-6416721: (fir de specificații) Fix Thread.yield() javadoc ) înregistrată pentru 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 numit HelloWorldApp.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: Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 3După cum puteți vedea, firul nostru are acum starea „În somn”. De fapt, există o modalitate mai elegantă de a ajuta firul nostru are vise dulci:

try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Woke up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
Ai observat că ne descurcăm InterruptedExceptionpeste 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 un InterruptedException. 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, whilebucla va fi executată până când firul este întrerupt extern. În ceea ce privește isInterruptedsteagul, 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: Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 4Datorită instrumentelor de monitorizare, poți vedea ce se întâmplă cu firul. Metoda joineste 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: Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 5Se pare că Java folosește un mecanism „monitor” pentru sincronizarea între fire. Fiecărui obiect este asociat un monitor, iar firele de execuție îl pot achiziționa 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: Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

Iată un articol JavaWorld care este foarte util: Cum realizează mașina virtuală Java sincronizarea firelor . Acest articol ar trebui să fie combinat cu descrierea din secțiunea „Rezumat” a următoarei probleme din sistemul de urmărire a erorilor JDK: JDK-8183909 . Puteți citi același lucru aici: JEP-8183909 . Deci, în Java, un monitor este asociat cu un obiect și este folosit pentru a bloca un fir atunci când firul de execuție încearcă să obțină (sau să obțină) blocarea. Iată cel mai simplu exemplu:

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 synchronizedcheie 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. Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 7

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 synchronizedbloc, 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: Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 8După cum puteți vedea în JVisualVM, starea este „Monitor”, ceea ce înseamnă că firul de execuție este blocat și nu poate prelua monitorul. De asemenea, puteți utiliza cod pentru a determina starea unui fir de execuție, dar numele stării determinate în acest fel nu se potrivesc cu numele folosite în JVisualVM, deși sunt similare. În acest caz,th1.getState()instrucțiunea din bucla for va returna BLOCKED , deoarece atâta timp cât bucla rulează, lockmonitorul obiectului este ocupat de mainfir, iar th1firul 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 HashTableclasă:

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 deosebire sleep()de și join(), această metodă nu poate fi numită pur și simplu. Numele lui este wait(). Metoda waiteste 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: Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 10Pentru a înțelege cum funcționează, amintiți-vă că metodele wait()și notify()sunt asociate cu java.lang.Object. Poate părea ciudat că metodele legate de fire sunt în Objectclasă. 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 lockmonitorul obiectului, dar va intra în coada de fire care așteaptă notificarea de la lockmonitorul 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 mainde 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, mainfirul în sine intră în lockblocul sincronizat al obiectului și efectuează notificarea firului folosind monitorul. După ce notificarea este trimisă, mainfirul elibereazălockmonitorul obiectului, iar noul fir de execuție, care anterior aștepta ca lockmonitorul 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. Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 11

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ți thread.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 synchronizedbloc.
  • AȘTEPTARE — așteptarea unui alt thread pentru a satisface o anumită condiție.
Dacă condiția este îndeplinită, atunci programatorul de fire pornește firul. Dacă firul așteaptă până la un timp specificat, atunci starea sa este TIMED_WAITING. Dacă firul de execuție nu mai rulează (s-a terminat sau s-a aruncat o excepție), atunci intră în starea TERMINAT. Pentru a afla starea unui fir, utilizați 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 . Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 12Această clasă asociază un „permis” cu fiecare fir care îl folosește. Un apel la park()metodă revine imediat dacă permisul este disponibil, consumând permisul în proces. În caz contrar, se blochează. Apelarea unparkmetodei face permisul disponibil dacă nu este încă disponibil. Există doar 1 permis. Documentația Java pentru LockSupportse referă la Semaphoreclasă. 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”. Mai bine împreună: Java și clasa Thread.  Partea a II-a — Sincronizare - 13Să ne uităm la un alt exemplu:

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 waitde la synchronizedcuvânt cheie și parkde la LockSupportclasă. De ce este asta LockSupportatâ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 LockSupport ș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 lockaș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 mainthread-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 Mai bine împreună: Java și clasa Thread. Partea a III-a — Interacțiunea Mai bine împreună: Java și clasa Thread. Partea a IV-a — Apelabil, viitor și prieteni Mai bine împreună: Java și clasa Thread. Partea V — Executor, ThreadPool, Fork/Join Better împreună: Java și clasa Thread. Partea a VI-a — Foc departe!
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION