ExecutorService in Android Java: guida completa all’uso corretto dei thread pool
In Android, gestire correttamente i thread è fondamentale per mantenere l’interfaccia utente fluida e reattiva. L’API ExecutorService di Java è uno degli strumenti più potenti e flessibili per eseguire operazioni in background, molto più scalabile rispetto a creare Thread manualmente uno per uno. In questo articolo vedremo come usare ExecutorService in Android Java, i diversi tipi di executor disponibili, esempi pratici con Callable, Future e runOnUiThread, la gestione di errori e shutdown, e un confronto con AsyncTask e thread tradizionali.

Perché usare ExecutorService in Android Java
In Android, tutto il codice che tocca la UI deve girare sul main thread (o UI thread). Operazioni lente come chiamate di rete, accesso al database o elaborazioni pesanti devi spostarle su thread in background per evitare blocchi, ANR (Application Not Responding) e una pessima esperienza utente. È qui che entra in gioco ExecutorService:
- Gestisce automaticamente un pool di thread riutilizzabili.
- Permette di inviare task (oggetti
RunnableoCallable) da eseguire in background. - Offre API avanzate per pianificare esecuzioni future, gestire il risultato con
Futuree controllare lo shutdown. - Riduce l’overhead di creazione/distruzione di thread, migliorando le prestazioni.
A differenza di creare manualmente un new Thread(...), con ExecutorService puoi centralizzare e controllare tutte le operazioni in background di un’app Android, ottenendo un codice più pulito, testabile e manutenibile.
Tipi di ExecutorService in Android Java
La classe Executors fornisce diversi metodi statici per creare implementazioni di ExecutorService. I più usati in Android sono:
newSingleThreadExecutor()newFixedThreadPool(int nThreads)newCachedThreadPool()newScheduledThreadPool(int corePoolSize)
newSingleThreadExecutor
newSingleThreadExecutor() crea un executor con un solo thread in background. Tutti i task girano in modo seriale, uno dopo l’altro, nell’ordine in cui li invii.
È utile quando vuoi garantire che un certo tipo di operazione non giri mai in parallelo, ad esempio scritture su disco o su database sensibili all’ordine.
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
singleExecutor.execute(new Runnable() {
@Override
public void run() {
// Codice eseguito in background, in modo seriale
saveUserToDatabase(user);
}
});
newFixedThreadPool
newFixedThreadPool(int nThreads) crea un pool con un numero fisso di thread. Se invii più task rispetto ai thread disponibili, il pool mette in coda i task extra.
È ideale quando vuoi limitare il grado di parallelismo, ad esempio per un insieme di operazioni di rete o di I/O che non devono saturare le risorse del dispositivo.
int NUMBER_OF_THREADS = 4;
ExecutorService fixedExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedExecutor.execute(new Runnable() {
@Override
public void run() {
// Esegui fino a 4 task in parallelo
downloadImage("https://example.com/image" + index + ".png");
}
});
}
newCachedThreadPool
newCachedThreadPool() crea un pool dinamico che può crescere e ridursi a seconda del carico. Riutilizza i thread inattivi per i nuovi task, ma se necessario ne crea di nuovi.
È adatto a eseguire molti task brevi e indipendenti tra loro, ma va usato con cautela in Android: se invii troppi task rischi di creare un numero eccessivo di thread e consumare molte risorse.
ExecutorService cachedExecutor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
cachedExecutor.execute(new Runnable() {
@Override
public void run() {
// Task veloce
logEventToServer();
}
});
}
newScheduledThreadPool
newScheduledThreadPool(int corePoolSize) permette di pianificare l’esecuzione futura di un task, oppure eseguirlo periodicamente. Implementa l’interfaccia ScheduledExecutorService, che estende ExecutorService.
Può essere utile in Android per operazioni ripetitive in background, come sincronizzazioni periodiche o cleanup, purché tu rispetti i vincoli di risparmio energetico e le linee guida di Android (in molti casi conviene usare WorkManager).
ScheduledExecutorService scheduledExecutor =
Executors.newScheduledThreadPool(2);
// Esegui un task una volta dopo 5 secondi
scheduledExecutor.schedule(new Runnable() {
@Override
public void run() {
syncDataWithServer();
}
}, 5, TimeUnit.SECONDS);
// Esegui un task ripetuto ogni 10 secondi
scheduledExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
cleanOldCache();
}
}, 0, 10, TimeUnit.SECONDS);
Esempio pratico: ExecutorService con Callable, Future e runOnUiThread
Uno degli scenari più comuni in Android è eseguire una operazione che restituisce un risultato dal background thread e poi aggiornare la UI con quel risultato. Con ExecutorService puoi farlo usando Callable<T> e Future<T>.
Supponiamo di dover caricare una lista di elementi da API e mostrarla in una RecyclerView. Useremo un ExecutorService per la chiamata di rete e runOnUiThread per aggiornare la UI:
public class MainActivity extends AppCompatActivity {
private ExecutorService executorService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
executorService = Executors.newFixedThreadPool(3);
loadItems();
}
private void loadItems() {
Callable<List<Item>> loadItemsTask = new Callable<List<Item>>() {
@Override
public List<Item> call() throws Exception {
// Questo gira in background
return apiClient.fetchItems();
}
};
final Future<List<Item>> future = executorService.submit(loadItemsTask);
executorService.execute(new Runnable() {
@Override
public void run() {
try {
final List<Item> items = future.get(); // Bloccante, ma siamo in background
runOnUiThread(new Runnable() {
@Override
public void run() {
// Questo gira sul main thread: aggiorna la UI
showItemsInRecyclerView(items);
}
});
} catch (final Exception e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
showErrorMessage(e.getMessage());
}
});
}
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
executorService.shutdownNow();
}
}
In questo esempio:
Callable<List<Item>>definisce un task che restituisce un valore e può lanciare eccezioni.submit()restituisce unFuture<List<Item>>, che rappresenta il risultato futuro.- In un altro task in background chiamiamo
future.get(), che blocca fino a quando i dati sono pronti. - Infine usiamo
runOnUiThread()per aggiornare in sicurezza laRecyclerViewcon i dati caricati.
In un codice di produzione potrebbe essere preferibile usare una sola chiamata a submit(Callable) e gestire il risultato all’interno dello stesso task, ma l’esempio mette in evidenza l’uso combinato di ExecutorService, Future e aggiornamento UI.
Gestione errori e shutdown di ExecutorService in Android
Quando usi ExecutorService in un’app Android è fondamentale:
- Gestire le eccezioni dei task in background.
- Chiudere correttamente il pool quando non serve più (ad esempio in
onDestroy()).
Gestione delle eccezioni
Se invii un Runnable con execute() e al suo interno lanci un’eccezione non catturata, questa non arriva al thread principale e diventa difficile da tracciare. È buona pratica avvolgere il codice in un blocco try/catch e propagare l’errore alla UI se necessario.
executorService.execute(new Runnable() {
@Override
public void run() {
try {
uploadFile(file);
} catch (final IOException e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
showErrorMessage("Upload fallito: " + e.getMessage());
}
});
}
}
});
Shutdown e gestione del ciclo di vita
Un ExecutorService mantiene vivi i thread finché non lo chiudi. In Android, se colleghi un executor a un componente con ciclo di vita limitato (Activity, Fragment, ViewModel), è importante chiamare shutdown() o shutdownNow() nel momento giusto, per evitare memory leak e spreco di risorse.
@Override
protected void onDestroy() {
super.onDestroy();
executorService.shutdown();
}
shutdown() impedisce l’invio di nuovi task ma lascia terminare quelli già in esecuzione. shutdownNow() prova a interrompere immediatamente i task in corso, restituendo una lista di quelli non ancora eseguiti. In molte app Android, shutdown() è la scelta più sicura, mentre shutdownNow() va usato solo se sei davvero certo di poter interrompere le operazioni in corso.
ExecutorService vs AsyncTask vs Thread: tabella comparativa
Storicamente, in Android gli sviluppatori usavano spesso AsyncTask e thread manuali per gestire le operazioni in background. Tuttavia, Google ha deprecato AsyncTask e lo sconsiglia, mentre l’uso diretto di Thread è poco scalabile. La combinazione di ExecutorService (o soluzioni più moderne come WorkManager e coroutine in Kotlin) è oggi la scelta più robusta.
| Caratteristica | ExecutorService | AsyncTask | Thread |
|---|---|---|---|
| Gestione dei thread | Pool riutilizzabile, configurabile | Gestita internamente, poco flessibile | Ogni task crea un nuovo thread |
| Stato API | Supportato e raccomandato (side Java) | Deprecato nelle ultime API | Supportato ma di basso livello |
| Ritorno di un risultato | Sì, con Callable/Future | Sì, con onPostExecute() | No, serve implementazione custom |
| Composizione di più task | Buona (invokeAll, Future, ecc.) | Limitata | Manuale e verbosa |
| Controllo sul parallelismo | Sì (single, fixed, cached…) | Limitato, dipende dalla versione | No, devi gestire tu il numero di thread |
| Integrazione con ciclo di vita | Manuale, ma chiara | Problematico, facile creare leak | Manuale, facilmente soggetto a errori |
| Facilità d’uso | Media: leggermente più complesso ma potente | Facile, ma ormai obsoleto | Semplice per casi banali, complesso per casi reali |
Se stai iniziando un nuovo progetto Android in Java, usare ExecutorService per la logica in background è una scelta molto più futura-proof rispetto a AsyncTask e più ordinata rispetto a creare Thread a mano.
Best practice per usare ExecutorService in Android Java
- Centralizza la creazione dell’ExecutorService: invece di creare un nuovo executor in ogni Activity, considera di gestirlo in un singleton, in un livello di data layer o in un
ViewModel, così puoi riutilizzare lo stesso pool. - Scegli il tipo di executor giusto: usa
newSingleThreadExecutor()per operazioni seriali sensibili all’ordine,newFixedThreadPool()per un numero limitato di task paralleli,newCachedThreadPool()solo se sei sicuro di non saturare il dispositivo. - Non bloccare il main thread: evita di chiamare
future.get()sul main thread, per non rischiare ANR. Se devi attendere un risultato, fallo in background e poi passa alla UI conrunOnUiThread()o Handler. - Rispetta il ciclo di vita: se l’executor è legato a una schermata, ricordati di chiamare
shutdown()inonDestroy(). Se invece deve vivere quanto l’app, valuta di integrarlo a livello di Application. - Gestisci i timeout: quando usi
Future, valuta di usareget(long timeout, TimeUnit unit)per evitare che un task bloccato rimanga per sempre in attesa. - Gestisci bene gli errori: cattura e registra le eccezioni, mostra messaggi chiari all’utente e, se necessario, permetti di riprovare l’operazione.
- Considera le alternative moderne: in molti casi, specialmente per lavori pianificati o che devono sopravvivere alla chiusura dell’app, è più adatto usare
WorkManager. Se usi Kotlin, coroutines eDispatchers.IOoffrono un modello ancora più espressivo che internamente può appoggiarsi a executor pool.
Conclusione
Usare ExecutorService in Android Java ti permette di gestire in modo ordinato ed efficiente tutte le operazioni in background della tua app, evitando i limiti e i problemi di strumenti più vecchi come AsyncTask e l’uso ingenuo di Thread singoli. Scegliendo il tipo di executor più adatto, gestendo correttamente Callable, Future e runOnUiThread, e chiudendo il pool al momento opportuno, puoi garantire un’esperienza utente fluida e un codice manutenibile.
Nei prossimi passi, puoi approfondire come funzionano i Thread in Java per capire meglio cosa c’è sotto il cofano di ExecutorService, e come collegare al meglio i risultati dei tuoi task in background a una RecyclerView performante e reattiva.
Rispondi