JavaRush /Java Blog /Random-IT /Multithreading in Java: essenza, vantaggi e insidie comun...

Multithreading in Java: essenza, vantaggi e insidie comuni

Pubblicato nel gruppo Random-IT
Ciao! Innanzitutto complimenti: sei arrivato al tema del Multithreading in Java! Questo è un risultato serio, c’è ancora molta strada da fare. Ma preparati: questo è uno degli argomenti più difficili del corso. E il punto non è che qui si utilizzino classi complesse o molti metodi: anzi, non ce ne sono nemmeno due dozzine. È più che altro che hai bisogno di cambiare un po' il tuo modo di pensare. In precedenza, i programmi venivano eseguiti in sequenza. Alcune righe di codice ne seguivano altre, alcuni metodi ne seguivano altri e nel complesso tutto era chiaro. Innanzitutto, calcola qualcosa, quindi visualizza il risultato sulla console, quindi termina il programma. Per comprendere il multithreading, è meglio pensare in termini di concorrenza. Cominciamo con qualcosa di molto semplice :) Multithreading in Java: essenza, vantaggi e insidie ​​​​comuni - 1Immagina che la tua famiglia si stia trasferendo da una casa all'altra. Una parte importante del trasloco è imballare i tuoi libri. Hai accumulato molti libri e devi metterli nelle scatole. Ora solo tu sei libero. La mamma sta preparando il cibo, il fratello sta raccogliendo i vestiti e la sorella è andata al negozio. Da solo puoi almeno farcela e, prima o poi, completerai anche tu il compito, ma ci vorrà molto tempo. Tuttavia, tra 20 minuti tua sorella tornerà dal negozio e non avrà altro da fare. Quindi può unirsi a te. Il compito è rimasto lo stesso: mettere i libri nelle scatole. Funziona semplicemente due volte più velocemente. Perché? Perché il lavoro viene svolto in parallelo. Due “fili” diversi (tu e tua sorella) svolgono contemporaneamente lo stesso compito e, se non cambia nulla, la differenza temporale sarà molto grande rispetto a una situazione in cui faresti tutto da solo. Se tuo fratello completa presto il suo compito, potrà aiutarti e le cose andranno ancora più velocemente.

Problemi risolti dal multithreading in Java

Essenzialmente, il multithreading Java è stato inventato per risolvere due problemi principali:
  1. Esegui più azioni contemporaneamente.

    Nell'esempio sopra, diversi thread (ad esempio i membri della famiglia) hanno eseguito diverse azioni in parallelo: lavare i piatti, andare al negozio, piegare le cose.

    Si può fornire un esempio più “programmatore”. Immagina di avere un programma con un'interfaccia utente. Quando si fa clic sul pulsante Continua, alcuni calcoli dovrebbero essere eseguiti all'interno del programma e l'utente dovrebbe vedere la seguente schermata di interfaccia. Se queste azioni vengono eseguite in sequenza, dopo aver fatto clic sul pulsante "Continua", il programma si bloccherà semplicemente. L'utente vedrà la stessa schermata con un pulsante “Continua” finché tutti i calcoli interni non saranno completati e il programma raggiungerà la parte in cui inizierà a disegnare l'interfaccia.

    Bene, aspettiamo un paio di minuti!

    Multithreading in Java: essenza, vantaggi e insidie ​​​​comuni - 3

    Possiamo anche rifare il nostro programma o, come dicono i programmatori, “parallelizzarlo”. Lascia che i calcoli necessari vengano eseguiti in un thread e il rendering dell'interfaccia in un altro. La maggior parte dei computer dispone di risorse sufficienti per questo. In questo caso, il programma non sarà “stupido” e l'utente si muoverà con calma tra le schermate dell'interfaccia senza preoccuparsi di ciò che accade all'interno. Non interferisce :)

  2. Velocizzare i calcoli.

    Qui è tutto molto più semplice. Se il nostro processore ha più core e la maggior parte dei processori è ora multi-core, il nostro elenco di attività può essere risolto in parallelo da più core. Ovviamente, se dobbiamo risolvere 1000 problemi e ognuno di essi viene risolto in un secondo, un core riuscirà a far fronte all'elenco in 1000 secondi, due core in 500 secondi, tre in poco più di 333 secondi e così via.

Ma, come hai già letto nella lezione, i sistemi moderni sono molto intelligenti e anche su un core di calcolo sono in grado di implementare il parallelismo, o pseudo-parallelismo, quando le attività vengono eseguite alternativamente. Passiamo dalle cose generali a quelle specifiche e facciamo conoscenza con la classe principale della libreria Java relativa al multithreading: java.lang.Thread. A rigor di termini, i thread in Java sono rappresentati da istanze della classe Thread. Cioè, per creare ed eseguire 10 thread, avrai bisogno di 10 oggetti di questa classe. Scriviamo l'esempio più semplice:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
Per creare e avviare thread, dobbiamo creare una classe ed ereditarla dal file java.lang. Threade sovrascrivere il metodo in esso contenuto run(). L'ultimo è molto importante. È nel metodo che run()prescriviamo la logica che il nostro thread deve eseguire. Ora, se creiamo un'istanza MyFirstThreade la eseguiamo, il metodo run()stamperà sulla console una riga con il suo nome: il metodo getName()stampa il nome “di sistema” del thread, che gli viene assegnato automaticamente. Anche se, in realtà, perché "se"? Creiamo e testiamo!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Output della console: sono Thread! Mi chiamo Thread-2, sono Thread! Mi chiamo Thread-1, sono Thread! Il mio nome è Thread-0, sono Thread! Mi chiamo Thread-3, sono Thread! Mi chiamo Thread-6, sono Thread! Mi chiamo Thread-7, sono Thread! Mi chiamo Thread-4, sono Thread! Mi chiamo Thread-5, sono Thread! Mi chiamo Thread-9, sono Thread! Il mio nome è Thread-8 Creiamo 10 thread (oggetti) MyFirstThreadche ereditano Threade li lanciamo chiamando il metodo dell'oggetto start(). Dopo aver chiamato un metodo , start()il suo metodo inizia a funzionare run()e la logica in esso scritta viene eseguita. Nota: i nomi dei thread non sono in ordine. È abbastanza strano, perché non sono stati giustiziati uno dopo l'altro: Thread-0, Thread-1, Thread-2e così via? Questo è esattamente un esempio di quando il pensiero “sequenziale” standard non funziona. Il fatto è che in questo caso emettiamo solo comandi per creare e avviare 10 thread. L'ordine in cui devono essere lanciati viene deciso dallo scheduler dei thread: uno speciale meccanismo interno al sistema operativo. Come è strutturato esattamente e in base a quale principio prende le decisioni è un argomento molto complesso e non lo approfondiremo ora. La cosa principale da ricordare è che il programmatore non può controllare la sequenza di esecuzione del thread. Per rendersi conto della gravità della situazione, prova a eseguire il metodo main()dell'esempio sopra ancora un paio di volte. Seconda uscita della console: Sono Thread! Il mio nome è Thread-0, sono Thread! Mi chiamo Thread-4, sono Thread! Mi chiamo Thread-3, sono Thread! Mi chiamo Thread-2, sono Thread! Mi chiamo Thread-1, sono Thread! Mi chiamo Thread-5, sono Thread! Mi chiamo Thread-6, sono Thread! Mi chiamo Thread-8, sono Thread! Mi chiamo Thread-9, sono Thread! Il mio nome è Thread-7 Terza uscita della console: sono Thread! Il mio nome è Thread-0, sono Thread! Mi chiamo Thread-3, sono Thread! Mi chiamo Thread-1, sono Thread! Mi chiamo Thread-2, sono Thread! Mi chiamo Thread-6, sono Thread! Mi chiamo Thread-4, sono Thread! Mi chiamo Thread-9, sono Thread! Mi chiamo Thread-5, sono Thread! Mi chiamo Thread-7, sono Thread! Il mio nome è Thread-8

Problemi creati dal multithreading

Nell'esempio con i libri, hai visto che il multithreading risolve problemi piuttosto importanti e il suo utilizzo accelera il lavoro dei nostri programmi. In molti casi, molte volte. Ma non per niente il multithreading è considerato un argomento complesso. Dopotutto, se usato in modo errato, crea problemi invece di risolverli. Quando dico “creare problemi” non intendo qualcosa di astratto. Esistono due problemi specifici che il multithreading può causare: deadlock e race condition. Il deadlock è una situazione in cui più thread attendono risorse occupate l'uno dall'altro e nessuno di essi può continuare l'esecuzione. Ne parleremo più approfonditamente nelle lezioni future, ma per ora questo esempio sarà sufficiente: Multithreading in Java: essenza, vantaggi e insidie ​​​​comuni - 4 immagina che il thread-1 stia lavorando con qualche Object-1 e il thread-2 stia lavorando con Object-2. Il programma è scritto così:
  1. Il Thread-1 smetterà di funzionare con l'Oggetto-1 e passerà all'Oggetto-2 non appena il Thread-2 smetterà di funzionare con l'Oggetto 2 e passerà all'Oggetto-1.
  2. Il Thread-2 smetterà di funzionare con l'Oggetto-2 e passerà all'Oggetto-1 non appena il Thread-1 smetterà di funzionare con l'Oggetto 1 e passerà all'Oggetto-2.
Anche senza una profonda conoscenza del multithreading, puoi facilmente capire che non ne verrà fuori nulla. I fili non cambieranno mai posto e si aspetteranno per sempre. L'errore sembra evidente, ma in realtà non lo è. Puoi facilmente consentirlo nel programma. Nelle lezioni seguenti esamineremo esempi di codice che causano una situazione di stallo. A proposito, Quora ha un eccellente esempio di vita reale che spiega cos'è un deadlock . “In alcuni stati dell’India, non ti venderanno terreni agricoli a meno che tu non sia registrato come agricoltore. Tuttavia, non sarai registrato come agricoltore se non possiedi terreni agricoli”. Ottimo, cosa posso dire! :) Ora parliamo delle condizioni della gara: lo stato della gara. Multithreading in Java: essenza, vantaggi e insidie ​​​​comuni - 5Una race condition è un difetto di progettazione in un sistema o un'applicazione multi-thread in cui il funzionamento del sistema o dell'applicazione dipende dall'ordine in cui vengono eseguite parti del codice. Ricorda l'esempio con i thread in esecuzione:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Ora immagina che il programma sia responsabile del funzionamento di un robot che prepara il cibo! Thread-0 toglie le uova dal frigorifero. Il flusso 1 accende i fornelli. Stream-2 tira fuori una padella e la mette sul fuoco. Stream 3 accende il fuoco sul fornello. Il flusso 4 versa l'olio nella padella. Il flusso 5 rompe le uova e le versa nella padella. Il flusso 6 getta i gusci nel cestino della spazzatura. Stream-7 rimuove le uova strapazzate finite dal fuoco. Potok-8 mette le uova strapazzate su un piatto. Il flusso 9 lava i piatti. Guarda i risultati del nostro programma: Thread-0 eseguito Thread-2 thread eseguito Thread-1 thread eseguito Thread-4 thread eseguito Thread-9 thread eseguito Thread-5 thread eseguito Thread-8 thread eseguito Thread-7 thread eseguito Thread-7 thread eseguito -3 Thread-6 thread eseguito Lo script è divertente? :) E tutto perché il funzionamento del nostro programma dipende dall'ordine in cui vengono eseguiti i thread. Alla minima violazione della sequenza, la nostra cucina si trasforma in un inferno e un robot impazzito distrugge tutto ciò che lo circonda. Questo è anche un problema comune nella programmazione multithread, di cui sentirai parlare più di una volta. Alla fine della conferenza, vorrei consigliarvi un libro sul multithreading.
Multithreading in Java: essenza, vantaggi e insidie ​​​​comuni - 6
"Java Concurrency in Practice" è stato scritto nel 2006, ma non ha perso la sua rilevanza. Copre la programmazione multithread in Java, partendo dalle basi e terminando con un elenco degli errori e degli antipattern più comuni. Se mai deciderai di diventare un guru della programmazione multithread, questo libro è assolutamente da leggere. Ci vediamo alle prossime lezioni! :)
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION