CIAO! Continuiamo il nostro studio del multithreading. Oggi conosceremo la volatileparola chiave e il yield()metodo. Immergiamoci :)

La parola chiave volatile

Durante la creazione di applicazioni multithread, possiamo incorrere in due seri problemi. Innanzitutto, quando un'applicazione multithread è in esecuzione, thread diversi possono memorizzare nella cache i valori delle variabili (ne abbiamo già parlato nella lezione intitolata 'Using volatile' ). Puoi avere la situazione in cui un thread cambia il valore di una variabile, ma un secondo thread non vede la modifica, perché sta lavorando con la sua copia cache della variabile. Naturalmente, le conseguenze possono essere gravi. Supponiamo che non sia solo una vecchia variabile, ma piuttosto il saldo del tuo conto bancario, che improvvisamente inizia a saltare su e giù in modo casuale :) Non sembra divertente, vero? In secondo luogo, in Java, le operazioni per leggere e scrivere tutti i tipi primitivi,longdouble, sono atomiche. Bene, per esempio, se modifichi il valore di una intvariabile su un thread e su un altro thread leggi il valore della variabile, otterrai il suo vecchio valore o quello nuovo, cioè il valore risultante dalla modifica nel thread 1. Non ci sono "valori intermedi". Tuttavia, questo non funziona con longs e doubles. Perché? A causa del supporto multipiattaforma. Ricordi ai livelli iniziali che abbiamo detto che il principio guida di Java è "scrivi una volta, esegui ovunque"? Ciò significa supporto multipiattaforma. In altre parole, un'applicazione Java viene eseguita su tutti i tipi di piattaforme diverse. Ad esempio, su sistemi operativi Windows, diverse versioni di Linux o MacOS. Funzionerà senza intoppi su tutti loro. Pesatura in 64 bit,longdoublesono le primitive "più pesanti" in Java. E alcune piattaforme a 32 bit semplicemente non implementano la lettura e la scrittura atomica di variabili a 64 bit. Tali variabili vengono lette e scritte in due operazioni. Innanzitutto, i primi 32 bit vengono scritti nella variabile, quindi vengono scritti altri 32 bit. Di conseguenza, potrebbe sorgere un problema. Un thread scrive un valore a 64 bit su una Xvariabile e lo fa in due operazioni. Allo stesso tempo, un secondo thread tenta di leggere il valore della variabile e lo fa tra queste due operazioni, quando i primi 32 bit sono stati scritti, ma i secondi 32 bit no. Di conseguenza, legge un valore intermedio errato e abbiamo un bug. Ad esempio, se su una piattaforma del genere proviamo a scrivere il numero a un 9223372036854775809 a una variabile, occuperà 64 bit. In forma binaria, ha questo aspetto: 100000000000000000000000000000000000000000000000000000000000000000000001 Il primo thread inizia a scrivere il numero nella variabile. Inizialmente scrive i primi 32 bit (10000000000000000000000000000000) e poi i secondi 32 bit (00000000000000000000000000000001) E il secondo thread può incastrarsi tra queste operazioni, leggendo il valore intermedio della variabile (1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 e e e e e e e/o per es. Nel sistema decimale, questo numero è 2.147.483.648. In altre parole, volevamo solo scrivere il numero 9223372036854775809 su una variabile, ma poiché questa operazione non è atomica su alcune piattaforme, abbiamo il numero malvagio 2,147,483,648, che è venuto fuori dal nulla e avrà un effetto sconosciuto programma. Il secondo thread ha semplicemente letto il valore della variabile prima che finisse di essere scritto, cioè il thread ha visto i primi 32 bit, ma non i secondi 32 bit. Naturalmente, questi problemi non si sono presentati ieri. Java li risolve con una singola parola chiave: volatile. Se usiamo ilvolatileparola chiave quando si dichiara una variabile nel nostro programma...

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 non avrai una situazione in cui 10 thread lavorano con le loro copie locali.
Quindi, due problemi molto seri vengono risolti con una sola parola :)

Il metodo yield()

Abbiamo già esaminato molti dei Threadmetodi della classe, ma ce n'è uno importante che sarà nuovo per te. È il yield()metodo . E fa esattamente quello che suggerisce il suo nome! Gestione dei thread.  La parola chiave volatile e il metodo yield() - 2Quando chiamiamo il yieldmetodo su un thread, in realtà parla agli altri thread: "Ehi, ragazzi. Non ho molta fretta di andare da nessuna parte, quindi se per qualcuno di voi è importante avere tempo di calcolo, prendetelo: posso aspettare'. 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() + " yields its place 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 avviamo in sequenza tre thread: Thread-0, Thread-1, e Thread-2. Thread-0parte per primo e cede subito agli altri. Poi Thread-1si avvia e cede anche. Quindi Thread-2viene avviato, che cede anche. Non abbiamo più thread e dopo Thread-2aver ceduto il suo posto per ultimo, lo scheduler dei thread dice: "Hmm, non ci sono più nuovi thread". Chi abbiamo in coda? Chi ha ceduto il suo posto prima Thread-2? Sembra che lo fosse Thread-1. Ok, questo significa che lo lasceremo correre'. Thread-1completa il suo lavoro e poi lo scheduler del thread continua il suo coordinamento: 'Okay, Thread-1finito. Abbiamo qualcun altro in coda?'. Thread-0 è in coda: ha ceduto il posto subito primaThread-1. Ora arriva il suo turno e corre fino al completamento. Quindi lo scheduler finisce di coordinare i thread: 'Okay, Thread-2hai ceduto ad altri thread e ora hanno finito. Sei stato l'ultimo a cedere, quindi ora tocca a te'. Quindi Thread-2corre fino al completamento. L'output della console sarà simile a questo: Thread-0 cede il suo posto ad altri Thread-1 cede il suo posto ad altri Thread-2 cede il suo posto ad altri Thread-1 ha terminato l'esecuzione. Thread-0 ha terminato l'esecuzione. Thread-2 ha terminato l'esecuzione. Naturalmente, lo scheduler dei thread potrebbe avviare i thread in un ordine diverso (ad esempio, 2-1-0 invece di 0-1-2), ma il principio rimane lo stesso.

Succede prima delle regole

L'ultima cosa che toccheremo oggi è il concetto di ' accade prima '. Come già sapete, in Java lo scheduler dei thread esegue la maggior parte del lavoro coinvolto nell'allocazione di tempo e risorse ai thread per eseguire le loro attività. Hai anche visto più volte come i thread vengono eseguiti in un ordine casuale che di solito è impossibile da prevedere. E in generale, dopo la programmazione "sequenziale" che abbiamo fatto in precedenza, la programmazione multithread sembra qualcosa di casuale. Sei già arrivato a credere che puoi utilizzare una serie di metodi per controllare il flusso di un programma multithread. Ma il multithreading in Java ha un altro pilastro: le 4 regole " accade prima ". Comprendere queste regole è abbastanza semplice. Immagina di avere due thread - AeB. Ciascuno di questi thread può eseguire operazioni 1e 2. In ogni regola, quando diciamo ' A succede prima di B ', intendiamo che tutte le modifiche apportate dal thread Aprima dell'operazione 1e le modifiche risultanti da questa operazione sono visibili al thread Bquando l'operazione 2viene eseguita e successivamente. Ogni regola garantisce che quando si scrive un programma multithread, alcuni eventi si verificheranno prima di altri il 100% delle volte e che al momento dell'operazione il 2thread Bsarà sempre a conoscenza delle modifiche Aapportate dal thread durante l'operazione 1. Rivediamoli.

Regola 1.

Il rilascio di un mutex avviene prima che lo stesso monitor venga acquisito da un altro thread. Penso che tu capisca tutto qui. Se il mutex di un oggetto o di una classe viene acquisito da un thread, ad esempio da thread A, un altro thread (thread B) non può acquisirlo contemporaneamente. Deve attendere fino al rilascio del mutex.

Regola 2.

Il Thread.start()metodo avviene prima Thread.run() . Ancora una volta, niente di difficile qui. Sai già che per iniziare a eseguire il codice all'interno del run()metodo, devi chiamare il start()metodo sul thread. Nello specifico, il metodo start, non il run()metodo stesso! Questa regola garantisce che i valori di tutte le variabili impostate prima Thread.start()della chiamata saranno visibili all'interno del run()metodo una volta avviato.

Regola 3.

La fine del run()metodo avviene prima del ritorno dal join()metodo. Torniamo ai nostri due thread: Ae B. Chiamiamo il join()metodo in modo che Bsia garantito che il thread attenda il completamento del thread Aprima che faccia il suo lavoro. Ciò significa che il metodo dell'oggetto A run()è garantito per essere eseguito fino alla fine. E tutte le modifiche ai dati che si verificano nel run()metodo del thread Asono garantite al cento per cento per essere visibili nel thread Buna volta che ha finito di aspettare che il thread Afinisca il suo lavoro in modo che possa iniziare il proprio lavoro.

Regola 4.

La scrittura su una volatilevariabile avviene prima della lettura dalla stessa variabile. Quando usiamo la volatileparola chiave, in realtà otteniamo sempre il valore corrente. Anche con una longo double(abbiamo parlato prima dei problemi che possono verificarsi qui). Come hai già capito, le modifiche apportate ad alcuni thread non sono sempre visibili ad altri thread. Ma, naturalmente, ci sono situazioni molto frequenti in cui tale comportamento non ci si addice. Supponiamo di assegnare un valore a una variabile sul thread A:

int z;

….

z = 555;
Se il nostro Bthread dovesse visualizzare il valore della zvariabile su console, potrebbe facilmente visualizzare 0, perché non conosce il valore assegnato. Ma la regola 4 garantisce che se dichiariamo la zvariabile come volatile, le modifiche al suo valore su un thread saranno sempre visibili su un altro thread. Se aggiungiamo alla parola volatileal codice precedente...

volatile int z;

….

z = 555;
... quindi preveniamo la situazione in cui il thread Bpotrebbe visualizzare 0. La scrittura sulle volatilevariabili avviene prima della lettura da esse.