Come avviare una discussione in Java

1. Introduzione

In questo tutorial, esploreremo diversi modi per avviare un thread ed eseguire attività parallele.

Questo è molto utile, in particolare quando si ha a che fare con operazioni lunghe o ricorrenti che non possono essere eseguite sul thread principale , o dove l'interazione dell'interfaccia utente non può essere sospesa in attesa dei risultati dell'operazione.

Per saperne di più sui dettagli dei thread, leggi sicuramente il nostro tutorial sul ciclo di vita di un thread in Java.

2. Le basi dell'esecuzione di un thread

Possiamo facilmente scrivere una logica che viene eseguita in un thread parallelo utilizzando il framework Thread .

Proviamo un esempio di base, estendendo la classe Thread :

public class NewThread extends Thread { public void run() { long startTime = System.currentTimeMillis(); int i = 0; while (true) { System.out.println(this.getName() + ": New Thread is running..." + i++); try { //Wait for one sec so it doesn't print too fast Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ... } } }

E ora scriviamo una seconda classe per inizializzare e avviare il nostro thread:

public class SingleThreadExample { public static void main(String[] args) { NewThread t = new NewThread(); t.start(); } }

Dovremmo chiamare il metodo start () sui thread nello stato NUOVO (l'equivalente di non avviato). In caso contrario, Java genererà un'istanza di eccezione IllegalThreadStateException .

Supponiamo ora di dover avviare più thread:

public class MultipleThreadsExample { public static void main(String[] args) { NewThread t1 = new NewThread(); t1.setName("MyThread-1"); NewThread t2 = new NewThread(); t2.setName("MyThread-2"); t1.start(); t2.start(); } }

Il nostro codice sembra ancora abbastanza semplice e molto simile agli esempi che possiamo trovare online.

Ovviamente, questo è lontano dal codice pronto per la produzione, in cui è di fondamentale importanza gestire le risorse nel modo corretto, per evitare un cambio di contesto eccessivo o un utilizzo eccessivo della memoria.

Quindi, per essere pronti per la produzione, ora dobbiamo scrivere boilerplate aggiuntivo per affrontare:

  • la creazione coerente di nuovi thread
  • il numero di thread live simultanei
  • la deallocazione dei thread: molto importante per i thread dei demoni al fine di evitare perdite

Se vogliamo, possiamo scrivere il nostro codice per tutti questi casi e anche altri, ma perché dovremmo reinventare la ruota?

3. ExecutorService Framework

I ExecutorService implementa il filo Pool modello di progettazione (chiamato anche un lavoratore o lavoratore-crew modello replicato) e si occupa della gestione dei thread abbiamo già detto, più esso aggiunge alcune funzionalità molto utili come un filo riutilizzabilità e attività code.

La riusabilità dei thread, in particolare, è molto importante: in un'applicazione su larga scala, l'allocazione e la deallocazione di molti oggetti thread crea un notevole sovraccarico di gestione della memoria.

Con i thread di lavoro, riduciamo al minimo il sovraccarico causato dalla creazione del thread.

Per facilitare la configurazione del pool, ExecutorService viene fornito con un semplice costruttore e alcune opzioni di personalizzazione, come il tipo di coda, il numero minimo e massimo di thread e la loro convenzione di denominazione.

Per maggiori dettagli su ExecutorService, leggi la nostra guida a Java ExecutorService.

4. Avvio di un'attività con gli esecutori

Grazie a questo potente framework, possiamo cambiare la nostra mentalità dall'avvio di thread all'invio di attività.

Diamo un'occhiata a come possiamo inviare un'attività asincrona al nostro esecutore:

ExecutorService executor = Executors.newFixedThreadPool(10); ... executor.submit(() -> { new Task(); });

Ci sono due metodi che possiamo usare: execute , che non restituisce nulla, e submit , che restituisce un Future che incapsula il risultato del calcolo.

Per ulteriori informazioni sui Futures, leggi la nostra Guida a java.util.concurrent.Future.

5. Avvio di un'attività con CompletableFutures

Per recuperare il risultato finale da un oggetto Future possiamo usare il metodo get disponibile nell'oggetto, ma questo bloccherebbe il thread genitore fino alla fine del calcolo.

In alternativa, potremmo evitare il blocco aggiungendo più logica al nostro compito, ma dobbiamo aumentare la complessità del nostro codice.

Java 1.8 ha introdotto un nuovo framework in cima al costrutto Future per lavorare meglio con il risultato del calcolo: il CompletableFuture .

CompletableFuture implementa CompletableStage , che aggiunge una vasta selezione di metodi per allegare callback ed evitare tutte le tubature necessarie per eseguire operazioni sul risultato dopo che è pronto.

L'implementazione per inviare un'attività è molto più semplice:

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync accetta un fornitore contenente il codice che vogliamo eseguire in modo asincrono, nel nostro caso il parametro lambda.

L'attività viene ora inviata implicitamente a ForkJoinPool.commonPool () , oppure possiamo specificare l' Executor che preferiamo come secondo parametro.

Per saperne di più su CompletableFuture, leggi la nostra guida a CompletableFuture.

6. Esecuzione di attività ritardate o periodiche

Quando si lavora con applicazioni web complesse, potrebbe essere necessario eseguire attività in momenti specifici, magari regolarmente.

Java ha pochi strumenti che possono aiutarci a eseguire operazioni ritardate o ricorrenti:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer è una funzione per pianificare le attività per l'esecuzione futura in un thread in background.

Le attività possono essere programmate per l'esecuzione una tantum o per l'esecuzione ripetuta a intervalli regolari.

Vediamo come appare il codice se vogliamo eseguire un'attività dopo un secondo di ritardo:

TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on: " + new Date() + "n" + "Thread's name: " + Thread.currentThread().getName()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; timer.schedule(task, delay);

Ora aggiungiamo una pianificazione ricorrente:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

Questa volta, l'attività verrà eseguita dopo il ritardo specificato e sarà ricorrente una volta trascorso il periodo di tempo.

Per ulteriori informazioni, leggi la nostra guida a Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor has methods similar to the Timer class:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); ScheduledFuture resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

To end our example, we use scheduleAtFixedRate() for recurring tasks:

ScheduledFuture resultFuture = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

The code above will execute a task after an initial delay of 100 milliseconds, and after that, it'll execute the same task every 450 milliseconds.

If the processor can't finish processing the task in time before the next occurrence, the ScheduledExecutorService will wait until the current task is completed, before starting the next.

To avoid this waiting time, we can use scheduleWithFixedDelay(), which, as described by its name, guarantees a fixed length delay between iterations of the task.

Per maggiori dettagli su ScheduledExecutorService, leggi la nostra guida a Java ExecutorService.

6.3. Quale strumento è migliore?

Se eseguiamo gli esempi precedenti, il risultato del calcolo sembra lo stesso.

Allora, come scegliamo lo strumento giusto ?

Quando un framework offre più scelte, è importante comprendere la tecnologia sottostante per prendere una decisione informata.

Proviamo a immergerci un po 'più a fondo sotto il cofano.

Timer :

  • non offre garanzie in tempo reale: le attività IT orari utilizzando l'Object.wait (lungo) il metodo
  • c'è un singolo thread in background, quindi le attività vengono eseguite in sequenza e un'attività di lunga durata può ritardarne altre
  • runtime exceptions thrown in a TimerTask would kill the only thread available, thus killing Timer

ScheduledThreadPoolExecutor:

  • can be configured with any number of threads
  • can take advantage of all available CPU cores
  • catches runtime exceptions and lets us handle them if we want to (by overriding afterExecute method from ThreadPoolExecutor)
  • cancels the task that threw the exception, while letting others continue to run
  • relies on the OS scheduling system to keep track of time zones, delays, solar time, etc.
  • provides collaborative API if we need coordination between multiple tasks, like waiting for the completion of all tasks submitted
  • provides better API for management of the thread life cycle

The choice now is obvious, right?

7. Difference Between Future and ScheduledFuture

In our code examples, we can observe that ScheduledThreadPoolExecutor returns a specific type of Future: ScheduledFuture.

ScheduledFuture extends both Future and Delayed interfaces, thus inheriting the additional method getDelay that returns the remaining delay associated with the current task. It's extended by RunnableScheduledFuture that adds a method to check if the task is periodic.

ScheduledThreadPoolExecutor implementa tutti questi costrutti tramite la classe interna ScheduledFutureTask e li utilizza per controllare il ciclo di vita dell'attività .

8. Conclusioni

In questo tutorial, abbiamo sperimentato i diversi framework disponibili per avviare thread ed eseguire attività in parallelo.

Quindi, siamo andati più in profondità nelle differenze tra Timer e ScheduledThreadPoolExecutor.

Il codice sorgente dell'articolo è disponibile su GitHub.