Esecutori newCachedThreadPool () vs newFixedThreadPool ()

1. Panoramica

Quando si tratta di implementazioni di pool di thread, la libreria standard Java offre molte opzioni tra cui scegliere. I pool di thread fissi e memorizzati nella cache sono piuttosto onnipresenti tra queste implementazioni.

In questo tutorial, vedremo come funzionano i pool di thread sotto il cofano e quindi confronteremo queste implementazioni ei loro casi d'uso.

2. Pool di thread memorizzato nella cache

Diamo un'occhiata a come Java crea un pool di thread memorizzati nella cache quando chiamiamo Executors.newCachedThreadPool () :

public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }

I pool di thread memorizzati nella cache utilizzano il "trasferimento sincrono" per accodare nuove attività. L'idea di base del trasferimento sincrono è semplice e tuttavia controintuitiva: si può mettere in coda un elemento se e solo se un altro thread prende quell'elemento allo stesso tempo. In altre parole, lo SynchronousQueue non può tenere qualsiasi attività di sorta.

Supponiamo che arrivi una nuova attività. Se c'è un thread inattivo in attesa sulla coda, il produttore dell'attività passa l'attività a quel thread. In caso contrario, poiché la coda è sempre piena, l'esecutore crea un nuovo thread per gestire tale attività .

Il pool memorizzato nella cache inizia con zero thread e può potenzialmente aumentare fino a contenere thread Integer.MAX_VALUE . In pratica, l'unica limitazione per un pool di thread memorizzati nella cache sono le risorse di sistema disponibili.

Per gestire meglio le risorse di sistema, i pool di thread memorizzati nella cache rimuoveranno i thread che rimangono inattivi per un minuto.

2.1. Casi d'uso

La configurazione del pool di thread memorizzati nella cache memorizza nella cache i thread (da cui il nome) per un breve periodo di tempo per riutilizzarli per altre attività. Di conseguenza, funziona meglio quando abbiamo a che fare con un numero ragionevole di attività di breve durata.

La chiave qui è "ragionevole" e "di breve durata". Per chiarire questo punto, valutiamo uno scenario in cui i pool memorizzati nella cache non sono adatti. Qui invieremo un milione di attività che impiegano 100 micro secondi ciascuna per essere completate:

Callable task = () -> { long oneHundredMicroSeconds = 100_000; long startedAt = System.nanoTime(); while (System.nanoTime() - startedAt  task).collect(toList()); var result = cachedPool.invokeAll(tasks);

Questo creerà molti thread che si traducono in un utilizzo della memoria irragionevole e, peggio ancora, in molti cambi di contesto della CPU. Entrambe queste anomalie danneggerebbero notevolmente le prestazioni complessive.

Pertanto, dovremmo evitare questo pool di thread quando il tempo di esecuzione è imprevedibile, come le attività associate a IO.

3. Pool di thread fisso

Vediamo come funzionano i pool di thread fissi sotto il cofano:

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

A differenza del pool di thread memorizzato nella cache, questo utilizza una coda illimitata con un numero fisso di thread senza scadenza . Pertanto, invece di un numero sempre crescente di thread, il pool di thread fissi tenta di eseguire le attività in ingresso con una quantità fissa di thread . Quando tutti i thread sono occupati, l'esecutore metterà in coda le nuove attività. In questo modo, abbiamo un maggiore controllo sul consumo di risorse del nostro programma.

Di conseguenza, i pool di thread fissi sono più adatti per attività con tempi di esecuzione imprevedibili.

4. Sfortunate somiglianze

Finora, abbiamo solo enumerato le differenze tra pool di thread memorizzati nella cache e fissi.

A parte tutte queste differenze, entrambi usano AbortPolicy come politica di saturazione . Pertanto, ci aspettiamo che questi esecutori generino un'eccezione quando non possono accettare e nemmeno mettere in coda altre attività.

Vediamo cosa succede nel mondo reale.

I pool di thread memorizzati nella cache continueranno a creare sempre più thread in circostanze estreme, quindi, praticamente, non raggiungeranno mai un punto di saturazione . Allo stesso modo, i pool di thread fissi continueranno ad aggiungere sempre più attività nella loro coda. Pertanto, anche le piscine fisse non raggiungeranno mai un punto di saturazione .

Poiché entrambi i pool non saranno saturati, quando il carico è eccezionalmente alto, consumeranno molta memoria per creare thread o attività di accodamento. Aggiungendo la beffa al danno, i pool di thread memorizzati nella cache subiranno anche molti cambi di contesto del processore.

Ad ogni modo, per avere un maggiore controllo sul consumo di risorse, si consiglia vivamente di creare un ThreadPoolExecutor personalizzato :

var boundedQueue = new ArrayBlockingQueue(1000); new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy()); 

Qui, il nostro pool di thread può avere fino a 20 thread e può mettere in coda fino a 1000 attività. Inoltre, quando non può accettare ulteriori carichi, genererà semplicemente un'eccezione.

5. conclusione

In questo tutorial, abbiamo dato un'occhiata al codice sorgente JDK per vedere come funzionano i diversi Executor sotto il cofano. Quindi, abbiamo confrontato i pool di thread fissi e memorizzati nella cache e i loro casi d'uso.

Alla fine, abbiamo cercato di affrontare il consumo di risorse fuori controllo di quei pool con pool di thread personalizzati.