newCachedThreadPool()
contro newFixedThreadPool()
Quando dovrei usare l'uno o l'altro? Quale strategia è migliore in termini di utilizzo delle risorse?
newCachedThreadPool()
contro newFixedThreadPool()
Quando dovrei usare l'uno o l'altro? Quale strategia è migliore in termini di utilizzo delle risorse?
Risposte:
Penso che i documenti spieghino abbastanza bene la differenza e l'uso di queste due funzioni:
Crea un pool di thread che riutilizza un numero fisso di thread che operano su una coda non associata condivisa. In ogni momento, al massimo i thread di nThreads saranno attività di elaborazione attive. Se vengono inviate attività aggiuntive quando tutti i thread sono attivi, attenderanno in coda fino a quando non sarà disponibile un thread. Se un thread termina a causa di un errore durante l'esecuzione prima dell'arresto, ne sostituirà uno nuovo, se necessario, per eseguire le attività successive. I thread nel pool esisteranno fino a quando non verranno esplicitamente arrestati.
Crea un pool di thread che crea nuovi thread secondo necessità, ma riutilizza i thread precedentemente creati quando sono disponibili. Questi pool in genere miglioreranno le prestazioni dei programmi che eseguono molte attività asincrone di breve durata. Le chiamate da eseguire riutilizzeranno i thread precedentemente creati, se disponibili. Se non è disponibile alcun thread esistente, verrà creato e aggiunto un nuovo thread al pool. I thread che non sono stati utilizzati per sessanta secondi vengono chiusi e rimossi dalla cache. Pertanto, un pool che rimane inattivo abbastanza a lungo non consumerà alcuna risorsa. Si noti che i pool con proprietà simili ma dettagli diversi (ad esempio parametri di timeout) possono essere creati utilizzando i costruttori ThreadPoolExecutor.
In termini di risorse, newFixedThreadPool
manterrà tutti i thread in esecuzione fino a quando non vengono esplicitamente terminati. Nei newCachedThreadPool
thread che non sono stati utilizzati per sessanta secondi vengono chiusi e rimossi dalla cache.
Detto questo, il consumo di risorse dipenderà molto dalla situazione. Ad esempio, se hai un numero enorme di attività di lunga durata, suggerirei il FixedThreadPool
. Per quanto riguarda il CachedThreadPool
documento, i documenti affermano che "Questi pool in genere miglioreranno le prestazioni dei programmi che eseguono molte attività asincrone di breve durata".
newCachedThreadPool
potrebbe causare seri problemi perché si lascia tutto il controllo su thread pool
e quando il servizio funziona con altri nello stesso host , il che potrebbe causare l'arresto anomalo degli altri a causa di una lunga attesa della CPU. Quindi penso che newFixedThreadPool
possa essere più sicuro in questo tipo di scenario. Anche questo post chiarisce le differenze più rilevanti tra loro.
Solo per completare le altre risposte, vorrei citare Effective Java, 2nd Edition, di Joshua Bloch, capitolo 10, articolo 68:
"La scelta del servizio di esecuzione per una particolare applicazione può essere complicata. Se stai scrivendo un piccolo programma o un server leggermente carico , usare Executors.new- CachedThreadPool è generalmente una buona scelta , poiché non richiede alcuna configurazione e in genere" cosa giusta." Ma un pool di thread nella cache non è una buona scelta per un server di produzione pesantemente caricato !
In un pool di thread memorizzati nella cache , le attività inviate non vengono messe in coda ma immediatamente passate a un thread per l'esecuzione. Se non sono disponibili thread, ne viene creato uno nuovo . Se un server è così pesantemente caricato che tutte le sue CPU sono completamente utilizzate e arrivano più attività, verranno creati più thread, il che peggiorerà le cose.
Pertanto, in un server di produzione pesantemente caricato , è molto meglio utilizzare Executors.newFixedThreadPool , che offre un pool con un numero fisso di thread o utilizzare direttamente la classe ThreadPoolExecutor, per il massimo controllo. "
Se guardi il codice sorgente , vedrai che stanno chiamando ThreadPoolExecutor. internamente e impostando le loro proprietà. Puoi crearne uno per avere un migliore controllo delle tue esigenze.
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Se non si è preoccupati di una coda illimitata di attività richiamabili / eseguibili , è possibile utilizzarne una. Come suggerito da bruno, anch'io preferisco newFixedThreadPool
di newCachedThreadPool
più di questi due.
Ma ThreadPoolExecutor offre funzionalità più flessibili rispetto a uno newFixedThreadPool
onewCachedThreadPool
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
vantaggi:
Hai il pieno controllo delle dimensioni di BlockingQueue . Non è illimitato, a differenza delle due precedenti opzioni. Non otterrò un errore di memoria esaurita a causa di un enorme accumulo di attività Callable / Runnable in sospeso quando c'è turbolenza imprevista nel sistema.
È possibile implementare criteri di gestione dei rifiuti personalizzati O utilizzare uno dei criteri:
Per impostazione predefinita ThreadPoolExecutor.AbortPolicy
, il gestore genera un runtime RejectedExecutionException al momento del rifiuto.
In ThreadPoolExecutor.CallerRunsPolicy
, il thread che invoca si esegue da solo esegue l'attività. Ciò fornisce un semplice meccanismo di controllo del feedback che rallenterà la velocità di invio di nuove attività.
In ThreadPoolExecutor.DiscardPolicy
, un'attività che non può essere eseguita viene semplicemente eliminata.
In ThreadPoolExecutor.DiscardOldestPolicy
, se l'esecutore non viene arrestato, l'attività in testa alla coda di lavoro viene eliminata e quindi l'esecuzione viene ritentata (il che può non riuscire di nuovo, provocando la ripetizione).
È possibile implementare una factory di thread personalizzata per i casi d'uso seguenti:
Esatto, Executors.newCachedThreadPool()
non è un'ottima scelta per il codice server che serve più client e richieste simultanee.
Perché? Ci sono sostanzialmente due problemi (correlati) con esso:
Non ha limiti, il che significa che stai aprendo la porta a chiunque possa paralizzare la tua JVM semplicemente iniettando più lavoro nel servizio (attacco DoS). I thread consumano una quantità non trascurabile di memoria e aumentano anche il consumo di memoria in base al loro lavoro in corso, quindi è abbastanza facile rovesciare un server in questo modo (a meno che non siano presenti altri interruttori).
Il problema illimitato è esacerbato dal fatto che l'Executor è fronteggiato da un SynchronousQueue
che significa che c'è un passaggio diretto tra il task giver e il pool di thread. Ogni nuova attività creerà un nuovo thread se tutti i thread esistenti sono occupati. Questa è generalmente una cattiva strategia per il codice del server. Quando la CPU si satura, le attività esistenti impiegano più tempo per terminare. Tuttavia, vengono inviate più attività e creati più thread, quindi le attività richiedono sempre più tempo per essere completate. Quando la CPU è satura, più thread sicuramente non sono ciò di cui il server ha bisogno.
Ecco i miei consigli:
Utilizzare un pool di thread di dimensioni fisse Executors.newFixedThreadPool o ThreadPoolExecutor. con un numero massimo di thread impostato;
La ThreadPoolExecutor
classe è l'implementazione di base per gli esecutori che vengono restituiti da molti Executors
metodi di fabbrica. Quindi affrontiamo i pool di thread fissi e memorizzati nella cache daThreadPoolExecutor
prospettiva.
Il costruttore principale di questa classe si presenta così:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
Il corePoolSize
determina la dimensione minima del pool di thread di destinazione.L'implementazione manterrebbe un pool di quelle dimensioni anche se non ci sono attività da eseguire.
Il maximumPoolSize
è il numero massimo di thread che possono essere attivi contemporaneamente.
Dopo che il pool di thread cresce e diventa più grande della corePoolSize
soglia, l'esecutore può terminare i thread inattivi e raggiungere corePoolSize
nuovamente. Se allowCoreThreadTimeOut
è vero, allora l'esecutore può persino terminare i thread del pool core se erano inattivi più della keepAliveTime
soglia.
Quindi la linea di fondo è se i thread rimangono inattivi più della keepAliveTime
soglia, potrebbero essere chiusi poiché non è richiesta per loro.
Cosa succede quando arriva una nuova attività e tutti i thread principali sono occupati? Le nuove attività verranno messe in coda all'interno di tale BlockingQueue<Runnable>
istanza. Quando un thread diventa libero, è possibile elaborare una di quelle attività in coda.
Esistono diverse implementazioni BlockingQueue
dell'interfaccia in Java, quindi possiamo implementare diversi approcci di accodamento come:
Coda limitata : le nuove attività verrebbero messe in coda all'interno di una coda attività limitata.
Coda senza limiti : le nuove attività verrebbero messe in coda all'interno di una coda di attività senza limiti. Quindi questa coda può crescere fino a quando la dimensione dell'heap lo consente.
Handoff sincrono : possiamo anche usare il SynchronousQueue
per mettere in coda le nuove attività. In tal caso, quando si mette in coda una nuova attività, un altro thread deve già essere in attesa di tale attività.
Ecco come ThreadPoolExecutor
esegue una nuova attività:
corePoolSize
sono in esecuzione thread, tenta di avviare un nuovo thread con l'attività specificata come primo lavoro.BlockingQueue#offer
metodo Il offer
metodo non si bloccherà se la coda è piena e ritorna immediatamentefalse
.offer
restituiscefalse
), quindi tenta di aggiungere un nuovo thread al pool di thread con questa attività come primo lavoro.RejectedExecutionHandler
.La differenza principale tra i pool di thread fissi e memorizzati nella cache si riduce a questi tre fattori:
+ ----------- + ----------- + ------------------- + ----- ---------------------------- + | Tipo di piscina | Dimensione del nucleo | Dimensione massima | Strategia di accodamento | + ----------- + ----------- + ------------------- + ----- ---------------------------- + | Risolto | n (fisso) | n (fisso) | `LinkedBlockingQueue` senza limiti | + ----------- + ----------- + ------------------- + ----- ---------------------------- + | Memorizzato nella cache | 0 | Integer.MAX_VALUE | `SynchronousQueue` | + ----------- + ----------- + ------------------- + ----- ---------------------------- +
Excutors.newFixedThreadPool(n)
funziona:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Come potete vedere:
OutOfMemoryError
.Quando dovrei usare l'uno o l'altro? Quale strategia è migliore in termini di utilizzo delle risorse?
Un pool di thread di dimensioni fisse sembra essere un buon candidato quando limiteremo il numero di attività simultanee ai fini della gestione delle risorse .
Ad esempio, se utilizzeremo un esecutore per gestire le richieste del server Web, un esecutore fisso può gestire i burst della richiesta in modo più ragionevole.
Per una gestione delle risorse ancora migliore, si consiglia vivamente di creare un'abitudine ThreadPoolExecutor
con BlockingQueue<T>
un'implementazione limitata abbinata a ragionevole RejectedExecutionHandler
.
Ecco come Executors.newCachedThreadPool()
funziona:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Come potete vedere:
Integer.MAX_VALUE
. In pratica, il pool di thread è illimitato.SynchronousQueue
fallisce sempre quando non c'è nessuno all'altra estremità per accettarla!Quando dovrei usare l'uno o l'altro? Quale strategia è migliore in termini di utilizzo delle risorse?
Usalo quando hai molte attività prevedibili di breve durata.
È necessario utilizzare newCachedThreadPool solo quando si hanno attività asincrone di breve durata come indicato in Javadoc, se si inoltrano attività che richiedono più tempo per l'elaborazione, si finirà per creare troppi thread. Puoi colpire la CPU al 100% se invii attività a esecuzione prolungata a una velocità maggiore a newCachedThreadPool ( http://rashcoder.com/be-careful-while-using-executors-newcachedthreadpool/ ).
Faccio alcuni test rapidi e ho i seguenti risultati:
1) se si utilizza SynchronousQueue:
Dopo che i thread hanno raggiunto la dimensione massima, qualsiasi nuova opera verrà rifiutata, ad eccezione di quella indicata di seguito.
Eccezione nel thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3fee733d rifiutata da java.util.concurrent.ThreadPoolExecutor@5acf9800 [Esecuzione, dimensioni pool = 3, thread attivi = 3, attività in coda = 0, attività completate = 0]
at java.util.concurrent.ThreadPoolExecutor $ AbortPolicy.rejectedExecution (ThreadPoolExecutor.java:2047)
2) se si utilizza LinkedBlockingQueue:
I thread non aumentano mai dalla dimensione minima alla dimensione massima, il che significa che il pool di thread ha dimensioni fisse come dimensione minima.