La JVM può presumere che altri thread non cambino la pizzaArrived
variabile durante il ciclo. In altre parole, può sollevare il pizzaArrived == false
test al di fuori del loop, ottimizzando questo:
while (pizzaArrived == false) {}
in questo:
if (pizzaArrived == false) while (true) {}
che è un ciclo infinito.
Per garantire che le modifiche apportate da un thread siano visibili ad altri thread, è sempre necessario aggiungere un po 'di sincronizzazione tra i thread. Il modo più semplice per farlo è creare la variabile condivisa volatile
:
volatile boolean pizzaArrived = false;
Creare una variabile volatile
garantisce che thread diversi vedranno gli effetti delle modifiche reciproche su di essa. Ciò impedisce alla JVM di memorizzare nella cache il valore pizzaArrived
o di sollevare il test all'esterno del loop. Invece, deve leggere ogni volta il valore della variabile reale.
(Più formalmente, volatile
crea una relazione accade prima tra gli accessi alla variabile. Ciò significa che tutto il resto del lavoro svolto da un thread prima di consegnare la pizza è visibile anche al thread che riceve la pizza, anche se quelle altre modifiche non riguardano le volatile
variabili.)
I metodi sincronizzati vengono utilizzati principalmente per implementare l'esclusione reciproca (impedendo che due cose accadano contemporaneamente), ma hanno anche tutti gli stessi effetti collaterali che volatile
ha. Usarli durante la lettura e la scrittura di una variabile è un altro modo per rendere visibili le modifiche ad altri thread:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (getPizzaArrived() == false) {}
System.out.println("That was delicious!");
}
synchronized boolean getPizzaArrived() {
return pizzaArrived;
}
synchronized void deliverPizza() {
pizzaArrived = true;
}
}
L'effetto di una dichiarazione di stampa
System.out
è un PrintStream
oggetto. I metodi di PrintStream
sono sincronizzati in questo modo:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
La sincronizzazione impedisce la pizzaArrived
memorizzazione nella cache durante il ciclo. In senso stretto, entrambi i thread devono sincronizzarsi sullo stesso oggetto per garantire che le modifiche alla variabile siano visibili. (Ad esempio, chiamare println
dopo l'impostazione pizzaArrived
e richiamarlo di nuovo prima della lettura pizzaArrived
sarebbe corretto.) Se solo un thread si sincronizza su un particolare oggetto, la JVM può ignorarlo. In pratica, la JVM non è abbastanza intelligente da dimostrare che altri thread non chiameranno println
dopo l'impostazione pizzaArrived
, quindi si presume che potrebbero farlo. Pertanto, non può memorizzare nella cache la variabile durante il ciclo se si chiama System.out.println
. Ecco perché i cicli come questo funzionano quando hanno un'istruzione print, sebbene non sia una correzione corretta.
L'utilizzo System.out
non è l'unico modo per causare questo effetto, ma è quello che le persone scoprono più spesso, quando cercano di eseguire il debug, perché il loro ciclo non funziona!
Il problema più grande
while (pizzaArrived == false) {}
è un ciclo di attesa impegnata. Questo è male! Mentre attende, divora la CPU, il che rallenta altre applicazioni e aumenta l'utilizzo di energia, la temperatura e la velocità della ventola del sistema. Idealmente, vorremmo che il thread del ciclo dormisse mentre attende, quindi non inghiotte la CPU.
Ecco alcuni modi per farlo:
Utilizzo di attesa / notifica
Una soluzione di basso livello consiste nell'usare i metodi di attesa / notifica diObject
:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
synchronized (this) {
while (!pizzaArrived) {
try {
this.wait();
} catch (InterruptedException e) {}
}
}
System.out.println("That was delicious!");
}
void deliverPizza() {
synchronized (this) {
pizzaArrived = true;
this.notifyAll();
}
}
}
In questa versione del codice, il thread del ciclo chiama wait()
, che mette il thread in sleep. Non utilizzerà alcun ciclo della CPU durante il sonno. Dopo che il secondo thread ha impostato la variabile, chiama notifyAll()
per riattivare tutti i thread che erano in attesa su quell'oggetto. È come avere il ragazzo della pizza suonare il campanello, così puoi sederti e riposarti mentre aspetti, invece di stare goffamente davanti alla porta.
Quando si chiama wait / notify su un oggetto, è necessario mantenere il blocco della sincronizzazione di quell'oggetto, che è ciò che fa il codice sopra. Puoi usare qualsiasi oggetto che ti piace purché entrambi i thread utilizzino lo stesso oggetto: qui ho usato this
(l'istanza di MyHouse
). Di solito, due thread non sarebbero in grado di entrare simultaneamente in blocchi sincronizzati dello stesso oggetto (che fa parte dello scopo della sincronizzazione) ma qui funziona perché un thread rilascia temporaneamente il blocco di sincronizzazione quando è all'interno del wait()
metodo.
BlockingQueue
A BlockingQueue
viene utilizzato per implementare le code produttore-consumatore. I "consumatori" prendono gli articoli dalla parte anteriore della coda e i "produttori" li spingono sul retro. Un esempio:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
// take next item from the queue (sleeps while waiting)
Object food = queue.take();
// and do something with it
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
// in producer threads, we push items on to the queue.
// if there is space in the queue we can return immediately;
// the consumer thread(s) will get to it later
queue.put("A delicious pizza");
}
}
Nota: i metodi put
e take
di BlockingQueue
possono generare InterruptedException
s, che sono eccezioni controllate che devono essere gestite. Nel codice precedente, per semplicità, le eccezioni vengono rilanciate. Potresti preferire catturare le eccezioni nei metodi e riprovare la chiamata put o take per assicurarti che abbia successo. A parte questo punto di bruttezza, BlockingQueue
è molto facile da usare.
Nessun'altra sincronizzazione è necessaria qui perché a si BlockingQueue
assicura che tutto ciò che i thread hanno fatto prima di inserire gli elementi nella coda sia visibile ai thread che portano fuori quegli elementi.
Esecutori
Executor
sono come messaggi già pronti BlockingQueue
che eseguono compiti. Esempio:
// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish
Per i dettagli si veda la documentazione per Executor
, ExecutorService
e Executors
.
Gestione degli eventi
Il looping mentre si attende che l'utente faccia clic su qualcosa in un'interfaccia utente è sbagliato. Utilizza invece le funzionalità di gestione degli eventi del toolkit dell'interfaccia utente. In Swing , ad esempio:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
// This event listener is run when the button is clicked.
// We don't need to loop while waiting.
label.setText("Button was clicked");
});
Poiché il gestore eventi viene eseguito sul thread di invio dell'evento, eseguire un lavoro lungo nel gestore eventi blocca altre interazioni con l'interfaccia utente fino al termine del lavoro. Le operazioni lente possono essere avviate su un nuovo thread o inviate a un thread in attesa utilizzando una delle tecniche precedenti (wait / notify, a BlockingQueue
o Executor
). Puoi anche usare a SwingWorker
, che è progettato esattamente per questo, e fornisce automaticamente un thread di lavoro in background:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {
// Defines MyWorker as a SwingWorker whose result type is String:
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
// This method is called on a background thread.
// You can do long work here without blocking the UI.
// This is just an example:
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
// This method is called on the Swing thread once the work is done
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result); // will display "Answer is 42"
}
}
// Start the worker
new MyWorker().execute();
});
Timer
Per eseguire azioni periodiche, puoi utilizzare un file java.util.Timer
. È più facile da usare che scrivere il proprio ciclo di temporizzazione e più facile da avviare e interrompere. Questa demo stampa l'ora corrente una volta al secondo:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Ognuno java.util.Timer
ha il proprio thread in background che viene utilizzato per eseguire i propri programmi pianificati TimerTask
. Naturalmente, il thread dorme tra le attività, quindi non inghiotte la CPU.
Nel codice Swing, c'è anche un javax.swing.Timer
, che è simile, ma esegue il listener sul thread Swing, quindi puoi interagire in sicurezza con i componenti Swing senza dover cambiare manualmente i thread:
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);
Altri modi
Se stai scrivendo codice multithread, vale la pena esplorare le classi in questi pacchetti per vedere cosa è disponibile:
E vedi anche la sezione Concorrenza dei tutorial Java. Il multithreading è complicato, ma è disponibile molto aiuto!