Una breve panoramica dei dettagli di come interagiscono i thread. In precedenza, abbiamo esaminato come i thread sono sincronizzati tra loro. Questa volta ci addentreremo nei problemi che possono sorgere quando i thread interagiscono e parleremo di come evitarli. Forniremo anche alcuni link utili per approfondimenti.
Con un plug-in JVisualVM installato (tramite Strumenti -> Plugin), possiamo vedere dove si è verificato il deadlock:
Secondo JVisualVM, vediamo periodi di sospensione e un periodo di parcheggio (questo è quando un thread tenta di acquisire un blocco - entra nello stato di parcheggio, come abbiamo discusso in precedenza quando abbiamo parlato della sincronizzazione dei thread ) . Puoi vedere un esempio di livelock qui: Java - Thread Livelock .
Puoi vedere un super esempio qui: Java - Thread Starvation and Fairness . Questo esempio mostra cosa succede con i thread durante la fame e come una piccola modifica da
Questa ispezione è stata aggiunta a IntelliJ IDEA come parte del problema IDEA-61117 , elencato nelle Note di rilascio nel 2010.

introduzione
Quindi, sappiamo che Java ha thread. Puoi leggerlo nella recensione intitolata Better together: Java and the Thread class. Parte I — Fili di esecuzione . E abbiamo esplorato il fatto che i thread possono sincronizzarsi tra loro nella recensione intitolata Better together: Java and the Thread class. Parte II — Sincronizzazione . È tempo di parlare di come i thread interagiscono tra loro. Come condividono le risorse condivise? Quali problemi potrebbero sorgere qui?
Punto morto
Il problema più spaventoso di tutti è lo stallo. Deadlock è quando due o più thread aspettano eternamente l'altro. Prenderemo un esempio dalla pagina web di Oracle che descrive deadlock :
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();
}
}
Il deadlock potrebbe non verificarsi qui la prima volta, ma se il tuo programma si blocca, allora è il momento di eseguire 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
Il thread 1 sta aspettando il blocco dal thread 0. Perché succede? Thread-1
avvia l'esecuzione ed esegue il Friend#bow
metodo. È contrassegnato con la synchronized
parola chiave, il che significa che stiamo acquisendo il monitor per this
(l'oggetto corrente). L'input del metodo era un riferimento all'altro Friend
oggetto. Ora, Thread-1
vuole eseguire il metodo sull'altro Friend
e deve acquisire il suo blocco per farlo. Ma se l'altro thread (in questo caso Thread-0
) è riuscito ad entrare nel bow()
metodo, allora il lock è già stato acquisito e Thread-1
attendeThread-0
, e viceversa. Questo vicolo cieco è irrisolvibile e lo chiamiamo deadlock. Come una presa mortale che non può essere rilasciata, lo stallo è un blocco reciproco che non può essere spezzato. Per un'altra spiegazione del deadlock, puoi guardare questo video: Deadlock and Livelock Explained .
Livelock
Se c'è un deadlock, c'è anche un livelock? Sì, c'è :) Il livelock si verifica quando i thread esteriormente sembrano essere vivi, ma non sono in grado di fare nulla, perché le condizioni richieste per continuare il loro lavoro non possono essere soddisfatte. Fondamentalmente, il livelock è simile al deadlock, ma i thread non "si bloccano" in attesa di un monitor. Invece, fanno sempre qualcosa. Per esempio:
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();
}
}
Il successo di questo codice dipende dall'ordine in cui lo scheduler dei thread Java avvia i thread. Se Thead-1
inizia prima, allora otteniamo 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
...
Come puoi vedere dall'esempio, entrambi i thread tentano di acquisire entrambi i blocchi a turno, ma falliscono. Ma non sono in stallo. Esteriormente, va tutto bene e stanno facendo il loro lavoro. 
Fame
Oltre a deadlock e livelock, c'è un altro problema che può verificarsi durante il multithreading: la fame. Questo fenomeno differisce dalle precedenti forme di blocco in quanto i thread non sono bloccati, semplicemente non dispongono di risorse sufficienti. Di conseguenza, mentre alcuni thread impiegano tutto il tempo di esecuzione, altri non sono in grado di eseguire:
https://www.logicbig.com/
Thread.sleep()
a Thread.wait()
consente di distribuire il carico in modo uniforme. 
Condizioni di gara
Nel multithreading esiste una "condizione di competizione". Questo fenomeno si verifica quando i thread condividono una risorsa, ma il codice è scritto in modo da non garantire una corretta condivisione. Dai un'occhiata a un esempio:
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();
}
}
Questo codice potrebbe non generare un errore la prima volta. Quando lo fa, potrebbe apparire così:
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)
Come puoi vedere, qualcosa è andato storto durante newValue
l'assegnazione di un valore. newValue
è troppo grande. A causa della race condition, uno dei thread è riuscito a modificare la variabile value
tra le due istruzioni. Si scopre che c'è una corsa tra i fili. Ora pensa a quanto sia importante non commettere errori simili con le transazioni monetarie... Esempi e diagrammi possono essere visti anche qui: Codice per simulare race condition in thread Java .
Volatile
Parlando dell'interazione dei thread,volatile
vale la pena menzionare la parola chiave. Diamo un'occhiata a un semplice esempio:
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;
}
}
La cosa più interessante è che è molto probabile che non funzioni. Il nuovo thread non vedrà la modifica nel flag
campo. Per risolvere questo problema per il flag
campo, dobbiamo utilizzare la volatile
parola chiave. Come e perché? Il processore esegue tutte le azioni. Ma i risultati dei calcoli devono essere archiviati da qualche parte. Per questo, c'è la memoria principale e c'è la cache del processore. Le cache di un processore sono come una piccola porzione di memoria utilizzata per accedere ai dati più rapidamente rispetto a quando si accede alla memoria principale. Ma tutto ha un rovescio della medaglia: i dati nella cache potrebbero non essere aggiornati (come nell'esempio sopra, quando il valore del campo flag non è stato aggiornato). Così ilvolatile
parola chiave dice alla JVM che non vogliamo memorizzare nella cache la nostra variabile. Ciò consente di visualizzare il risultato aggiornato su tutti i thread. Questa è una spiegazione molto semplificata. Per quanto riguarda la volatile
parola chiave, consiglio vivamente di leggere questo articolo . Per maggiori informazioni, ti consiglio anche di leggere Java Memory Model e Java Volatile Keyword . Inoltre, è importante ricordare che volatile
si tratta della visibilità e non dell'atomicità dei cambiamenti. Osservando il codice nella sezione "Condizioni di gara", vedremo un suggerimento in IntelliJ IDEA: 
Atomicita
Le operazioni atomiche sono operazioni che non possono essere divise. Ad esempio, l'operazione di assegnazione di un valore a una variabile deve essere atomica. Sfortunatamente, l'operazione di incremento non è atomica, perché l'incremento richiede fino a tre operazioni della CPU: ottieni il vecchio valore, aggiungine uno, quindi salva il valore. Perché l'atomicità è importante? Con l'operazione di incremento, se c'è una race condition, allora la risorsa condivisa (cioè il valore condiviso) può cambiare improvvisamente in qualsiasi momento. Inoltre, le operazioni che coinvolgono strutture a 64 bit, ad esempiolong
e double
, non sono atomiche. Maggiori dettagli possono essere letti qui: Garantire l'atomicità durante la lettura e la scrittura di valori a 64 bit . I problemi relativi all'atomicità possono essere visti in questo esempio:
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());
}
}
La classe speciale AtomicInteger
ci darà sempre 30.000, ma value
cambierà di volta in volta. C'è una breve panoramica di questo argomento: Introduzione alle variabili atomiche in Java . L'algoritmo "compare-and-swap" è al centro delle classi atomiche. Puoi leggere di più qui in Confronto tra algoritmi senza blocco - CAS e FAA sull'esempio di JDK 7 e 8 o nell'articolo Confronta e scambia su Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Succede prima
C'è un concetto interessante e misterioso chiamato "succede prima". Come parte del tuo studio sui thread, dovresti leggerlo. La relazione succede prima mostra l'ordine in cui verranno visualizzate le azioni tra i thread. Ci sono molte interpretazioni e commenti. Ecco una delle presentazioni più recenti su questo argomento: Java "Happens-Before" Relationships .Riepilogo
In questa recensione, abbiamo esplorato alcune delle specifiche di come interagiscono i thread. Abbiamo discusso dei problemi che potrebbero sorgere, nonché dei modi per identificarli ed eliminarli. Elenco di materiali aggiuntivi sull'argomento:- Chiusura a doppio controllo
- Domande frequenti su JSR 133 (modello di memoria Java).
- QI 35: come prevenire una situazione di stallo?
- Concetti di concorrenza in Java di Douglas Hawkins (2017)
GO TO FULL VERSION