Problemi risolti dal multithreading in Java
Essenzialmente, il multithreading Java è stato inventato per risolvere due problemi principali:-
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!
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 :)
-
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.
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
. Thread
e 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 MyFirstThread
e 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) MyFirstThread
che ereditano Thread
e 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-2
e 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: immagina che il thread-1 stia lavorando con qualche Object-1 e il thread-2 stia lavorando con Object-2. Il programma è scritto così:- 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.
- 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.
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.
GO TO FULL VERSION