JavaRush /Java Blog /Random-IT /Gestione dei flussi. La parola chiave volatile e il metod...

Gestione dei flussi. La parola chiave volatile e il metodo yield()

Pubblicato nel gruppo Random-IT
Ciao! Continuiamo a studiare il multithreading e oggi faremo conoscenza con una nuova parola chiave: volatile e il metodo yield(). Scopriamo di cosa si tratta :)

Parola chiave volatile

Quando creiamo applicazioni multi-thread, possiamo affrontare due seri problemi. In primo luogo, durante il funzionamento di un'applicazione multi-thread, diversi thread possono memorizzare nella cache i valori delle variabili (ne parleremo più approfonditamente nella lezione "Utilizzo di volatile" ). È possibile che un thread abbia modificato il valore di una variabile, ma il secondo non abbia notato questo cambiamento perché stava lavorando con la propria copia della variabile memorizzata nella cache. Naturalmente le conseguenze possono essere gravi. Immagina che questa non sia solo una sorta di "variabile", ma, ad esempio, il saldo della tua carta di credito, che improvvisamente ha iniziato a saltare avanti e indietro in modo casuale :) Non molto piacevole, vero? In secondo luogo, in Java, le operazioni di lettura e scrittura su campi di tutti i tipi tranne longe doublesono atomiche. Cos'è l'atomicità? Bene, ad esempio, se modifichi il valore di una variabile in un thread inte in un altro thread leggi il valore di questa variabile, otterrai il suo vecchio valore o uno nuovo, quello che si è rivelato dopo la modifica in discussione 1. Nessuna "opzione intermedia" apparirà lì Forse. Tuttavia, questo non funziona con longe . doublePerché? Perché è multipiattaforma. Ricordi come dicevamo ai primi livelli che il principio Java è “scritto una volta, funziona ovunque”? Questo è multipiattaforma. Cioè, un'applicazione Java viene eseguita su piattaforme completamente diverse. Ad esempio, sui sistemi operativi Windows, su diverse versioni di Linux o MacOS e ovunque questa applicazione funzionerà stabilmente. longe double- le primitive più “pesanti” in Java: pesano 64 bit. E alcune piattaforme a 32 bit semplicemente non implementano l'atomicità della lettura e della scrittura di variabili a 64 bit. Tali variabili vengono lette e scritte in due operazioni. Nella variabile vengono prima scritti i primi 32 bit, poi altri 32. Di conseguenza, in questi casi può sorgere un problema. Un thread scrive un valore a 64 bit su una variabileХ, e lo fa “in due passi”. Allo stesso tempo, il secondo thread tenta di leggere il valore di questa variabile, e lo fa proprio a metà, quando i primi 32 bit sono già stati scritti, ma i secondi non sono ancora stati scritti. Di conseguenza, legge un valore intermedio errato e si verifica un errore. Ad esempio, se su una piattaforma del genere proviamo a scrivere un numero in una variabile - 9223372036854775809 - occuperà 64 bit. In formato binario sarà simile a questo: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 Il primo thread inizierà a scrivere questo numero in una variabile e scriverà prima i primi 32 bit: 10000000000000000000000 00000 00000 e poi il secondo 32: 00000000000000000000000000000001 E un secondo thread può incunearsi in questo divario e leggere il valore intermedio della variabile - 1000000000000000000000000000000000, i primi 32 bit che sono già stati scritti. Nel sistema decimale, questo numero è uguale a 2147483648. Cioè, volevamo solo scrivere il numero 9223372036854775809 in una variabile, ma poiché questa operazione su alcune piattaforme non è atomica, abbiamo ottenuto il numero "sinistro" 2147483648 , di cui non abbiamo bisogno, dal nulla e non si sa come influenzerà il funzionamento del programma. Il secondo thread legge semplicemente il valore della variabile prima che venga scritta definitivamente, ovvero vede i primi 32 bit, ma non i secondi 32 bit. Questi problemi, ovviamente, non si sono verificati ieri e in Java vengono risolti utilizzando una sola parola chiave: volatile . Se dichiariamo qualche variabile nel nostro programma con la parola volatile...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…significa che:
  1. Sarà sempre letto e scritto atomicamente. Anche se è a 64 bit doubleo long.
  2. La macchina Java non lo memorizzerà nella cache. Quindi è esclusa la situazione in cui 10 thread lavorano con le loro copie locali.
Ecco come si risolvono due problemi molto seri in una parola :)

metodo yield()

Abbiamo già esaminato molti metodi della classe Thread, ma ce n'è uno importante che sarà nuovo per te. Questo è il metodo yield() . Tradotto dall'inglese come "arrendersi". Ed è esattamente ciò che fa il metodo! Gestione dei flussi.  La parola chiave volatile e il metodo yield() - 2Quando chiamiamo il metodo yield su un thread, in realtà dice agli altri thread: "Okay, ragazzi, non ho particolare fretta, quindi se è importante per qualcuno di voi avere tempo di CPU, prendetelo, sono non urgente." Ecco un semplice esempio di come funziona:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Creiamo e lanciamo in sequenza tre thread: Thread-0, Thread-1e Thread-2. Thread-0inizia per primo e lascia subito il posto agli altri. Dopo di ciò inizia Thread-1e cede anche il passo. Dopodiché inizia Thread-2, che è anche inferiore. Non abbiamo più thread e dopo che Thread-2l'ultimo ha ceduto il suo posto, lo scheduler dei thread appare: “Allora non ci sono più nuovi thread, chi abbiamo in coda? Chi è stato l'ultimo a cedere il posto prima Thread-2? Penso che fosse Thread-1? Ok, allora lasciamo che sia fatto." Thread-1fa il suo lavoro fino alla fine, dopodiché lo scheduler dei thread continua a coordinarsi: “Okay, Thread-1 è stato completato. Abbiamo qualcun altro in fila?" C'è il Thread-0 in coda: ha ceduto il suo posto immediatamente prima del Thread-1. Ora la questione è arrivata a lui e lo stanno portando avanti fino alla fine. Dopodiché lo scheduler termina di coordinare i thread: “Va bene, Thread-2, hai lasciato il posto ad altri thread, hanno già funzionato tutti. Sei stato l’ultimo a cedere, quindi ora tocca a te”. Successivamente, Thread-2 viene eseguito fino al completamento. L'output della console sarà simile al seguente: Thread-0 cede il passo agli altri Thread-1 cede il posto ad altri Thread-2 cede il posto ad altri Thread-1 ha terminato l'esecuzione. L'esecuzione del thread-0 è terminata. L'esecuzione del thread-2 è terminata. Lo scheduler dei thread, ovviamente, può eseguire i thread in un ordine diverso (ad esempio, 2-1-0 invece di 0-1-2), ma il principio è lo stesso.

Regole "succede prima".

L'ultima cosa di cui parleremo oggi sono i principi del " succede prima ". Come già saprai, in Java, la maggior parte del lavoro di allocazione di tempo e risorse ai thread per completare le proprie attività viene svolto dallo scheduler dei thread. Inoltre, hai visto più di una volta come i thread vengono eseguiti in un ordine arbitrario e molto spesso è impossibile prevederlo. E in generale, dopo la programmazione “sequenziale” che abbiamo fatto prima, il multithreading sembra una cosa casuale. Come hai già visto, l'avanzamento di un programma multithread può essere controllato utilizzando tutta una serie di metodi. Ma oltre a questo, nel multithreading Java c'è un'altra "isola di stabilità" - 4 regole chiamate " accade prima ". Letteralmente dall'inglese questo è tradotto come “accade prima”, o “accade prima”. Il significato di queste regole è abbastanza semplice da capire. Immagina di avere due thread: Ae B. Ciascuno di questi thread può eseguire operazioni 1e 2. E quando in ciascuna delle regole diciamo " A accade prima di B ", ciò significa che tutte le modifiche apportate dal thread Aprima dell'operazione 1e le modifiche che questa operazione ha comportato sono visibili al thread Bnel momento in cui viene eseguita l'operazione 2e dopo che l'operazione è stata eseguita. Ognuna di queste regole garantisce che durante la scrittura di un programma multi-thread, alcuni eventi accadranno prima di altri il 100% delle volte e che il thread Bal momento dell'operazione 2sarà sempre a conoscenza delle modifiche Аapportate dal thread durante l'operazione 1. Diamo un'occhiata a loro.

Regola 1.

Il rilascio di un mutex avviene prima che avvenga prima che un altro thread acquisisca lo stesso monitor. Bene, qui sembra tutto chiaro. Se il mutex di un oggetto o di una classe viene acquisito da un thread, ad esempio, un thread А, un altro thread (thread B) non può acquisirlo contemporaneamente. È necessario attendere il rilascio del mutex.

Regola 2.

Il metodo Thread.start() accade prima Thread.run() . Niente di complicato neanche. Lo sai già: affinché il codice all'interno del metodo inizi l'esecuzione run(), devi chiamare il metodo sul thread start(). È suo, e non il metodo in sé run()! Questa regola garantisce che Thread.start()i valori di tutte le variabili impostate prima dell'esecuzione saranno visibili all'interno del metodo che ha avviato l'esecuzione run().

Regola 3.

Il completamento del metodo run() avviene prima dell'uscita dal metodo join(). Torniamo ai nostri due flussi - Аe B. Chiamiamo il metodo join()in modo tale che il thread Bdebba attendere fino al completamento Aprima di eseguire il proprio lavoro. Ciò significa che il metodo run()dell'oggetto A funzionerà sicuramente fino alla fine. E tutte le modifiche ai dati che si verificano nel metodo run()thread Asaranno completamente visibili nel thread Bquando attenderà il completamento Ae inizierà a funzionare da solo.

Regola 4.

La scrittura su una variabile volatile avviene prima della lettura dalla stessa variabile. Utilizzando la parola chiave volatile, infatti, otterremo sempre il valore corrente. Anche nel caso di longe double, i cui problemi sono stati discussi in precedenza. Come già capirai, le modifiche apportate in alcuni thread non sono sempre visibili ad altri thread. Ma, ovviamente, molto spesso ci sono situazioni in cui tale comportamento del programma non è adatto a noi. Diciamo che abbiamo assegnato un valore a una variabile in un thread A:
int z;.

z= 555;
Se il nostro thread Bdovesse stampare il valore di una variabile zsulla console, potrebbe facilmente stampare 0 perché non conosce il valore ad essa assegnato. Quindi, la Regola 4 ci garantisce: se dichiari una variabile zcome volatile, le modifiche ai suoi valori in un thread saranno sempre visibili in un altro thread. Se aggiungiamo la parola volatile al codice precedente...
volatile int z;.

z= 555;
...è esclusa la situazione in cui lo stream Brestituirà 0 alla console. La scrittura su variabili volatili avviene prima della lettura da esse.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION