O scurtă prezentare generală a detaliilor despre modul în care interacționează firele. Anterior, ne-am uitat la modul în care firele sunt sincronizate între ele. De data aceasta ne vom scufunda în problemele care pot apărea pe măsură ce firele de discuție interacționează și vom vorbi despre cum să le evităm. De asemenea, vom oferi câteva link-uri utile pentru un studiu mai aprofundat.
Cu un plugin JVisualVM instalat (prin Instrumente -> Plugins), putem vedea unde a apărut blocajul:
Potrivit JVisualVM, vedem perioade de somn și o perioadă de parcare (acesta este momentul în care un fir încearcă să obțină o blocare - intră în starea de parcare, așa cum am discutat mai devreme când am vorbit despre sincronizarea firului ) . Puteți vedea un exemplu de livelock aici: Java - Thread Livelock .
Puteți vedea un super exemplu aici: Java - Thread Starvation and Fairness . Acest exemplu arată ce se întâmplă cu firele în timpul înfometării și cum o mică schimbare de la
Această inspecție a fost adăugată la IntelliJ IDEA ca parte a ediției IDEA-61117 , care a fost listată în Notele de lansare încă din 2010.

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 . Și am explorat faptul că firele de execuție se pot sincroniza unele cu altele în recenzia intitulată Better together: Java and the Thread class. Partea a II-a — Sincronizare . Este timpul să vorbim despre cum interacționează firele între ele. Cum împărtășesc resursele comune? Ce probleme ar putea apărea aici?
Impas
Cea mai înfricoșătoare problemă dintre toate este blocajul. Deadlock este atunci când două sau mai multe fire îl așteaptă veșnic pe celălalt. Vom lua un exemplu de pe pagina web Oracle care descrie blocajul :
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
Este posibil să nu apară blocarea aici prima dată, dar dacă programul dvs. se blochează, atunci este timpul să rulați jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Firul 1 așteaptă blocarea de la firul 0. De ce se întâmplă asta? Thread-1
începe să ruleze și execută Friend#bow
metoda. Este marcat cu synchronized
cuvântul cheie, ceea ce înseamnă că achiziționăm monitorul pentru this
(obiectul curent). Intrarea metodei a fost o referire la celălalt Friend
obiect. Acum, Thread-1
vrea să execute metoda pe celălalt Friend
și trebuie să-și obțină blocarea pentru a face acest lucru. Dar dacă celălalt fir (în acest caz Thread-0
) a reușit să intre în bow()
metodă, atunci blocarea a fost deja achiziționată și Thread-1
așteaptăThread-0
, si invers. Acesta este un impas de nerezolvat, iar noi îl numim impas. Ca o strângere de moarte care nu poate fi eliberată, blocajul este o blocare reciprocă care nu poate fi întreruptă. Pentru o altă explicație a blocajului, puteți urmări acest videoclip: Deadlock and Livelock Explained .
Livelock
Dacă există blocaj, există și blocaj? Da, există :) Livelock se întâmplă atunci când firele par să fie vii în exterior, dar nu pot face nimic, deoarece nu pot fi îndeplinite condițiile necesare pentru a-și continua munca. Practic, livelock este similar cu deadlock, dar firele nu se „atârnă” în așteptarea unui monitor. În schimb, ei fac mereu ceva. De exemplu:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
Succesul acestui cod depinde de ordinea în care programatorul de fire Java pornește firele. Dacă Thead-1
începe mai întâi, atunci obținem livelock:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
După cum puteți vedea din exemplu, ambele fire încearcă să obțină ambele blocări pe rând, dar nu reușesc. Dar, ei nu sunt în impas. În exterior, totul este bine și își fac treaba. 
Foame
Pe lângă blocaj și blocaj, există o altă problemă care se poate întâmpla în timpul multithreadingului: înfometarea. Acest fenomen diferă de formele anterioare de blocare prin faptul că firele de execuție nu sunt blocate - pur și simplu nu au resurse suficiente. Drept urmare, în timp ce unele fire de execuție iau tot timpul de execuție, altele nu pot rula:
https://www.logicbig.com/
Thread.sleep()
a Thread.wait()
vă permite să distribuiți sarcina uniform. 
Condiții de cursă
În multithreading, există o „condiție de cursă”. Acest fenomen se întâmplă atunci când firele de discuție partajează o resursă, dar codul este scris într-un mod care nu asigură partajarea corectă. Aruncă o privire la un exemplu:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Este posibil ca acest cod să nu genereze o eroare prima dată. Când se întâmplă, poate arăta astfel:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
După cum puteți vedea, ceva a mers prost în timp ce newValue
i s-a atribuit o valoare. newValue
este prea mare. Din cauza condiției de cursă, unul dintre fire a reușit să schimbe variabilele value
dintre cele două instrucțiuni. Se pare că există o cursă între fire. Acum gândiți-vă cât de important este să nu faceți greșeli similare cu tranzacțiile monetare... Exemple și diagrame pot fi văzute și aici: Cod pentru a simula condiția de cursă în firul Java .
Volatil
Vorbind despre interacțiunea firelor,volatile
cuvântul cheie merită menționat. Să ne uităm la un exemplu simplu:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
Cel mai interesant este că acest lucru este foarte probabil să nu funcționeze. Noul thread nu va vedea schimbarea în flag
domeniu. Pentru a remedia acest lucru pentru flag
câmp, trebuie să folosim volatile
cuvântul cheie. Cum și de ce? Procesorul efectuează toate acțiunile. Dar rezultatele calculelor trebuie stocate undeva. Pentru aceasta, există memorie principală și există memoria cache a procesorului. Cache-urile unui procesor sunt ca o mică bucată de memorie folosită pentru a accesa datele mai rapid decât atunci când accesați memoria principală. Dar totul are un dezavantaj: este posibil ca datele din cache să nu fie actualizate (ca în exemplul de mai sus, când valoarea câmpului steag nu a fost actualizată). Asa cavolatile
cuvântul cheie îi spune JVM-ului că nu dorim să ne memorăm variabila în cache. Acest lucru permite ca rezultatul actualizat să fie văzut pe toate firele. Aceasta este o explicație foarte simplificată. În ceea ce privește volatile
cuvântul cheie, vă recomand să citiți acest articol . Pentru mai multe informații, vă sfătuiesc, de asemenea, să citiți Java Memory Model și Java Volatile Keyword . În plus, este important să ne amintim că volatile
este vorba despre vizibilitate, și nu despre atomicitatea modificărilor. Privind codul din secțiunea „Condiții de cursă”, vom vedea un sfat explicativ în IntelliJ IDEA: 
Atomicitatea
Operațiile atomice sunt operații care nu pot fi împărțite. De exemplu, operația de atribuire a unei valori unei variabile trebuie să fie atomică. Din păcate, operația de creștere nu este atomică, deoarece incrementarea necesită până la trei operații CPU: obțineți valoarea veche, adăugați una la ea, apoi salvați valoarea. De ce este importantă atomicitatea? Cu operația de creștere, dacă există o condiție de cursă, atunci resursa partajată (adică valoarea partajată) se poate schimba brusc în orice moment. În plus, operațiunile care implică structuri pe 64 de biți, de exemplulong
și double
, nu sunt atomice. Mai multe detalii pot fi citite aici: Asigurați atomicitatea atunci când citiți și scrieți valori pe 64 de biți . Problemele legate de atomicitate pot fi văzute în acest exemplu:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
Clasa specială AtomicInteger
ne va oferi întotdeauna 30.000, dar se value
va schimba din când în când. Există o scurtă prezentare generală a acestui subiect: Introducere în variabilele atomice în Java . Algoritmul de „comparare și schimb” se află în centrul claselor atomice. Puteți citi mai multe despre asta aici în Comparația algoritmilor fără blocare - CAS și FAA pe exemplul JDK 7 și 8 sau în articolul Compare-and-swap de pe Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Se întâmplă-înainte
Există un concept interesant și misterios numit „se întâmplă înainte”. Ca parte a studiului dvs. de fire, ar trebui să citiți despre asta. Relația întâmplă-înainte arată ordinea în care vor fi văzute acțiunile dintre fire. Există multe interpretări și comentarii. Iată una dintre cele mai recente prezentări pe acest subiect: Relațiile Java „Se întâmplă-Înainte” .rezumat
În această recenzie, am explorat câteva dintre detaliile modului în care firele de discuție interacționează. Am discutat despre problemele care pot apărea, precum și despre modalitățile de identificare și eliminare. Lista de materiale suplimentare pe această temă:- Încuiere dublu verificată
- Întrebări frecvente despre JSR 133 (Java Memory Model).
- IQ 35: Cum să preveniți un impas?
- Concepte de concurență în Java de Douglas Hawkins (2017)
GO TO FULL VERSION