9 errori da evitare durante la programmazione in Java
Quali sono gli errori da evitare durante la programmazione in Java? Nel pezzo che segue rispondiamo a questa domanda.
Leggete la prima parte della nostra serie di blog dedicata alla concorrenza in Java. Nel prossimo articolo daremo uno sguardo più approfondito alle differenze tra thread e processi, ai pool di thread, agli esecutori e a molto altro ancora!
In generale, l'approccio convenzionale alla programmazione è sequenziale. Tutto in un programma avviene un passo alla volta.
Ma, in realtà, il parallelo è il modo in cui funziona il mondo intero: è la capacità di eseguire più di un compito contemporaneamente.
Per discutere di argomenti avanzati come concomitanza in Java o multithreading, dobbiamo trovare delle definizioni comuni per essere sicuri di essere sulla stessa lunghezza d'onda.
Cominciamo con le basi. Nel mondo non sequenziale, abbiamo due tipi di rappresentazioni della concorrenza: processi e
thread. Un processo è un'istanza del programma in esecuzione. Normalmente, è isolato dagli altri processi.
Il sistema operativo è responsabile dell'assegnazione delle risorse a ciascun processo. Inoltre, agisce come un conduttore che
programma e controlla.
Il thread è una sorta di processo ma a un livello inferiore, pertanto è noto anche come thread leggero. Più thread possono essere eseguiti in un
processo. In questo caso il programma agisce come scheduler e controllore dei thread. In questo modo i singoli programmi sembrano fare
più compiti contemporaneamente.
La differenza fondamentale tra thread e processi è il livello di isolamento. Il processo ha un proprio insieme di
mentre il thread condivide i dati con altri thread. Può sembrare un approccio a rischio di errore, e in effetti lo è. Per
Ora, non concentriamoci su questo aspetto perché esula dallo scopo di questo articolo.
Processi, thread - ok... Ma cos'è esattamente la concorrenza? Concorrenza significa che è possibile eseguire più attività allo stesso tempo.
tempo. Non significa che questi compiti debbano essere eseguiti simultaneamente: questo è il parallelismo. Concurrenc in Javay inoltre non
richiede la presenza di più CPU o addirittura di più core. È possibile ottenerlo in un ambiente single-core sfruttando
commutazione di contesto.
Un termine correlato alla concorrenza è il multithreading. Si tratta di una caratteristica dei programmi che consente loro di eseguire più compiti contemporaneamente. Non tutti i programmi utilizzano questo approccio, ma quelli che lo fanno possono essere definiti multithread.
Siamo quasi pronti, manca solo una definizione. Asincronia significa che un programma esegue operazioni non bloccanti.
Inizia un compito e poi continua a fare altre cose in attesa della risposta. Quando riceve la risposta, può reagire.
Per impostazione predefinita, ogni Applicazione Java viene eseguito in un processo. In questo processo, c'è un thread relativo al file main()
metodo di
un'applicazione. Tuttavia, come accennato, è possibile sfruttare i meccanismi di più thread all'interno di un'unica applicazione.
programma.
Il filo
è un Java in cui avviene la magia. Questa è la rappresentazione ad oggetti del thread di cui sopra. A
creare un proprio thread, è possibile estendere il metodo Il filo
classe. Tuttavia, non è un approccio consigliato. Fili
deve essere usato come meccanismo per eseguire il task. I task sono pezzi di codice che vogliamo eseguire in modalità concorrente. Possiamo definirli utilizzando l'opzione Eseguibile
interfaccia.
Ma basta con la teoria, mettiamo il nostro codice al posto della bocca.
Supponiamo di avere una coppia di matrici di numeri. Per ogni matrice, vogliamo conoscere la somma dei numeri in una matrice. Facciamo
Si supponga che ci siano molti array di questo tipo e che ognuno di essi sia relativamente grande. In queste condizioni, vogliamo sfruttare la concorrenza e sommare ogni array come compito separato.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. La somma è: " + sum);
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. La somma è: " + sum);
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. La somma è: " + sum);
};
nuovo Thread(task1).start();
nuovo Thread(task2).start();
nuovo Thread(task3).start();
Come si può vedere dal codice qui sopra Eseguibile
è un'interfaccia funzionale. Contiene un singolo metodo astratto eseguire()
senza argomenti. Il Eseguibile
dovrebbe essere implementata da qualsiasi classe le cui istanze siano destinate ad essere
eseguito da un thread.
Una volta definito un task, è possibile creare un thread per eseguirlo. Questo può essere fatto tramite nuovo thread()
costruttore che
prende Eseguibile
come argomento.
Il passo finale consiste nel start()
un thread appena creato. Nell'API ci sono anche eseguire()
metodi in Eseguibile
e inIl filo
. Tuttavia, questo non è un modo per sfruttare la concorrenza in Java. Una chiamata diretta a ciascuno di questi metodi risulta in
eseguire l'attività nello stesso thread in cui si trova il main()
viene eseguito il metodo.
Quando ci sono molti task, creare un thread separato per ciascuno di essi non è una buona idea. Creare un thread Il filo
è un
È un'operazione pesante ed è molto meglio riutilizzare i thread esistenti piuttosto che crearne di nuovi.
Quando un programma crea molti thread di breve durata, è meglio utilizzare un pool di thread. Il pool di thread contiene un certo numero di
filoni pronti per essere eseguiti ma attualmente non attivi. Dare un Eseguibile
al pool fa sì che uno dei thread richiami il metodoeseguire()
metodo di data Eseguibile
. Dopo aver completato un'attività, il thread esiste ancora e si trova in uno stato di inattività.
Ok, avete capito: preferite i pool di thread alla creazione manuale. Ma come si possono utilizzare i pool di thread? Il Esecutori
ha una serie di metodi statici di fabbrica per la costruzione di pool di thread. Ad esempio nuovoCachedThredPool()
crea
un pool in cui vengono creati nuovi thread in base alle necessità e i thread inattivi vengono mantenuti per 60 secondi. Al contrario,newFixedThreadPool()
contiene un insieme fisso di thread, in cui i thread inattivi vengono mantenuti indefinitamente.
Vediamo come potrebbe funzionare nel nostro esempio. Ora non è necessario creare manualmente i thread. Dobbiamo invece creareServizio Esecutore
che fornisce un pool di thread. Poi si possono assegnare i compiti. L'ultimo passo è chiudere il thread
per evitare perdite di memoria. Il resto del codice precedente rimane invariato.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
Eseguibile
sembra un modo intelligente per creare task concorrenti, ma ha un grosso difetto. Non può restituire alcun
valore. Inoltre, non possiamo stabilire se un'attività è terminata o meno. Non sappiamo nemmeno se è stato completato.
normalmente o eccezionalmente. La soluzione a questi mali è Callable
.
Callable
è simile a Eseguibile
in un certo senso avvolge anche i task asincroni. La differenza principale è che è in grado di
restituiscono un valore. Il valore di ritorno può essere di qualsiasi tipo (non primitivo), come il parametro Callable
è un tipo parametrizzato.Callable
è un'interfaccia funzionale che ha chiamare()
che può lanciare un metodo Eccezione
.
Vediamo ora come fare leva su Callable
nel nostro problema dell'array.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. La somma è: " + future1.get());
System.out.println("2. La somma è: " + future2.get());
System.out.println("3. La somma è: " + future3.get());
executor.shutdown();
Ok, possiamo vedere come Callable
viene creato e poi inviato a Servizio Esecutore
. Ma cosa diavolo è Futuro
?Futuro
funge da ponte tra i thread. La somma di ogni array viene prodotta in un thread separato e abbiamo bisogno di un modo per
riportare i risultati a main()
.
Per recuperare il risultato da Futuro
dobbiamo chiamare get()
metodo. In questo caso possono accadere due cose. In primo luogo, il metodo
risultato del calcolo eseguito da Callable
è disponibile. Allora lo otteniamo immediatamente. In secondo luogo, il risultato non è
pronto. In questo caso get()
si bloccherà finché il risultato non sarà disponibile.
Il problema con Futuro
è che funziona secondo il "paradigma push". Quando si utilizza Futuro
devi essere come un capo che
chiede costantemente: "Il tuo compito è finito? È pronto?" finché non fornisce un risultato. Agire sotto costante pressione è
costoso. Un approccio decisamente migliore sarebbe quello di ordinare Futuro
cosa fare quando è pronto per il suo compito. Purtroppo,Futuro
non può farlo, ma Futuro Computabile
può.
Futuro Computabile
funziona secondo il "paradigma pull". Possiamo dirgli cosa fare con il risultato quando ha completato i suoi compiti. Esso
è un esempio di approccio asincrono.
Futuro Computabile
funziona perfettamente con Eseguibile
ma non con Callable
. Invece, è possibile fornire un compito aFuturo Computabile
sotto forma di Fornitore
.
Vediamo come quanto detto sopra si collega al nostro problema.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
La prima cosa che colpisce è quanto sia più corta questa soluzione. Inoltre, ha un aspetto pulito e ordinato.
Compito di CompletabileFuturo
può essere fornito da supplyAsync()
che prende il metodo Fornitore
o da runAsync()
che
prende Eseguibile
. Un callback - un pezzo di codice che deve essere eseguito al completamento dell'attività - è definito da thenAccept()
metodo.
Java offre molti approcci diversi alla concorrenza. In questo articolo abbiamo appena accennato all'argomento.
Tuttavia, abbiamo trattato le basi di Il filo
, Eseguibile
, Callable
, e CallableFuture
che pone una buona questione
per approfondire l'argomento.