Impossibile creare un pool di thread nella cache con un limite di dimensioni?


127

Sembra impossibile creare un pool di thread nella cache con un limite al numero di thread che può creare.

Ecco come viene implementato Executors.newCachedThreadPool statico nella libreria Java standard:

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

Quindi, usando quel modello per continuare a creare un pool di thread nella cache di dimensioni fisse:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

Ora se lo usi e invii 3 attività, tutto andrà bene. L'invio di ulteriori attività comporterà eccezioni di esecuzione rifiutate.

Provando questo:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

Il risultato sarà l'esecuzione sequenziale di tutti i thread. Vale a dire, il pool di thread non realizzerà mai più di un thread per gestire le attività.

Questo è un bug nel metodo di esecuzione di ThreadPoolExecutor? O forse questo è intenzionale? O c'è un altro modo?

Modifica: voglio qualcosa di simile al pool di thread nella cache (crea thread su richiesta e poi li uccide dopo un certo timeout) ma con un limite al numero di thread che può creare e la possibilità di continuare a mettere in coda attività aggiuntive una volta che ha ha raggiunto il limite del thread. Secondo la risposta di Sjlee questo è impossibile. Guardando il metodo execute () di ThreadPoolExecutor è davvero impossibile. Avrei bisogno di sottoclassare ThreadPoolExecutor e sovrascrivere execute () in qualche modo come fa SwingWorker, ma ciò che SwingWorker fa nel suo execute () è un hack completo.


1
Qual è la tua domanda? Il tuo secondo codice non è forse la risposta al tuo titolo?
rsp,

4
Voglio un pool di thread che aggiungerà thread su richiesta con l'aumentare del numero di attività, ma non aggiungerà mai più di un numero massimo di thread. CachedThreadPool lo fa già, tranne che aggiungerà un numero illimitato di thread e non si fermerà ad una dimensione predefinita. La dimensione che definisco negli esempi è 3. Il secondo esempio aggiunge 1 thread, ma non ne aggiunge altri due quando arrivano nuove attività mentre le altre attività non sono ancora state completate.
Matt Crinklaw-Vogt,

Controlla questo, lo risolve, debuggingisfun.blogspot.com/2012/05/…
ethan,

Risposte:


235

ThreadPoolExecutor ha i seguenti comportamenti chiave diversi e i tuoi problemi possono essere spiegati da questi comportamenti.

Quando vengono inoltrate attività,

  1. Se il pool di thread non ha raggiunto la dimensione del core, crea nuovi thread.
  2. Se la dimensione del core è stata raggiunta e non ci sono thread inattivi, mette in coda le attività.
  3. Se la dimensione del core è stata raggiunta, non ci sono thread inattivi e la coda si riempie, crea nuovi thread (fino a raggiungere la dimensione massima).
  4. Se è stata raggiunta la dimensione massima, non ci sono thread inattivi e la coda si riempie, entra in vigore la politica di rifiuto.

Nel primo esempio, nota che SynchronousQueue ha essenzialmente una dimensione di 0. Pertanto, nel momento in cui raggiungi la dimensione massima (3), inizia la politica di rifiuto (# 4).

Nel secondo esempio, la coda di scelta è LinkedBlockingQueue che ha una dimensione illimitata. Pertanto, rimani bloccato con il comportamento n. 2.

Non puoi davvero armeggiare molto con il tipo memorizzato nella cache o il tipo fisso, poiché il loro comportamento è quasi completamente determinato.

Se si desidera disporre di un pool di thread limitato e dinamico, è necessario utilizzare una dimensione core positiva e una dimensione massima combinate con una coda di dimensioni finite. Per esempio,

new ThreadPoolExecutor(10, // core size
    50, // max size
    10*60, // idle timeout
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(20)); // queue with a size

Addendum : questa è una risposta abbastanza vecchia e sembra che JDK abbia cambiato comportamento quando si tratta di dimensioni del nucleo pari a 0. Da JDK 1.6, se la dimensione del nucleo è 0 e il pool non ha alcun thread, ThreadPoolExecutor aggiungerà un thread per eseguire quell'attività. Pertanto, la dimensione del nucleo di 0 è un'eccezione alla regola sopra. Grazie Steve per averlo portato alla mia attenzione.


4
Devi scrivere alcune parole sul metodo allowCoreThreadTimeOutper rendere perfetta questa risposta. Vedi la risposta di @utente1046052
hsestupin

1
Bella risposta! Un solo punto da aggiungere: vale la pena menzionare anche altre politiche di rifiuto. Vedi la risposta di @brianegge
Jeff

1
Il comportamento 2 non dovrebbe dire "Se la dimensione maxThread è stata raggiunta e non ci sono thread inattivi, mette in coda le attività". ?
Zoltán,

1
Potresti approfondire cosa implica la dimensione della coda? Significa che solo 20 attività possono essere messe in coda prima che vengano rifiutate?
Zoltán,

1
@ Zoltán L'ho scritto qualche tempo fa, quindi c'è una possibilità che alcuni comportamenti possano essere cambiati da allora (non ho seguito le ultime attività troppo da vicino), ma supponendo che questi comportamenti siano invariati, il n. 2 è corretto come indicato, e questo è forse il punto più importante (e in qualche modo sorprendente) di questo. Una volta raggiunta la dimensione del core, TPE favorisce l'accodamento rispetto alla creazione di nuovi thread. La dimensione della coda è letteralmente la dimensione della coda passata al TPE. Se la coda diventa piena ma non ha raggiunto la dimensione massima, creerà un nuovo thread (non rifiuta le attività). Vedi # 3. Spero che aiuti.
sjlee

60

A meno che non mi sia perso qualcosa, la soluzione alla domanda originale è semplice. Il codice seguente implementa il comportamento desiderato come descritto dal poster originale. Genererà fino a 5 thread per lavorare su una coda illimitata e i thread inattivi termineranno dopo 60 secondi.

tp = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>());
tp.allowCoreThreadTimeOut(true);

1
Hai ragione. Questo metodo è stato aggiunto in jdk 1.6, quindi non come molte persone ne sono a conoscenza. inoltre, non è possibile avere una dimensione del pool core "min", il che è un peccato.
jtahlborn,

4
La mia unica preoccupazione al riguardo è (dai documenti di JDK 8): "Quando viene inviata una nuova attività nel metodo execute (Runnable) e viene eseguito un numero inferiore di thread corePoolSize, viene creato un nuovo thread per gestire la richiesta, anche se un altro lavoratore i thread sono inattivi. "
Veegee,

Abbastanza sicuro che in realtà non funziona. L'ultima volta che ho cercato di fare quanto sopra in realtà esegue il tuo lavoro in un solo thread anche se hai generato 5. Ancora una volta, sono passati alcuni anni ma quando mi sono tuffato nell'implementazione di ThreadPoolExecutor è stato inviato a nuovi thread solo quando la coda era piena. L'uso di una coda illimitata non lo fa mai. Puoi provare inviando lavoro e registrando il nome della discussione e poi dormendo. Ogni file eseguibile finirà per stampare lo stesso nome / non essere eseguito su nessun altro thread.
Matt Crinklaw-Vogt,

2
Questo funziona, Matt. Hai impostato la dimensione del core su 0, ecco perché hai avuto solo 1 thread. Il trucco qui è impostare la dimensione del nucleo sulla dimensione massima.
T-Gergely,

1
@vegee ha ragione - In realtà non funziona molto bene - ThreadPoolExecutor riutilizza i thread solo sopra corePoolSize. Pertanto, quando corePoolSize è uguale a maxPoolSize, trarrai vantaggio dalla memorizzazione nella cache del thread solo quando il pool è pieno (quindi se intendi utilizzarlo ma di solito rimani al di sotto della dimensione massima del pool, potresti anche ridurre il timeout del thread su un valore basso valore; e tieni presente che non c'è cache - sempre nuovi thread)
Chris Riddell il

7

Ha avuto lo stesso problema. Poiché nessun'altra risposta mette insieme tutti i problemi, sto aggiungendo il mio:

Ora è chiaramente scritto nei documenti : se si utilizza una coda che non blocca ( LinkedBlockingQueue) l'impostazione dei thread max non ha alcun effetto, vengono utilizzati solo i thread core.

così:

public class MyExecutor extends ThreadPoolExecutor {

    public MyExecutor() {
        super(4, 4, 5,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        allowCoreThreadTimeOut(true);
    }

    public void setThreads(int n){
        setMaximumPoolSize(Math.max(1, n));
        setCorePoolSize(Math.max(1, n));
    }

}

Questo esecutore ha:

  1. Nessun concetto di thread max poiché stiamo utilizzando una coda illimitata. Questa è una buona cosa perché tale coda può causare all'esecutore la creazione di un numero enorme di thread extra non core se segue la sua politica abituale.

  2. Una coda di dimensioni massime Integer.MAX_VALUE. Submit()verrà lanciato RejectedExecutionExceptionse il numero di attività in sospeso supera Integer.MAX_VALUE. Non sono sicuro che prima saremo a corto di memoria o ciò accadrà.

  3. Ha 4 thread core possibili. I thread core inattivi terminano automaticamente se inattivi per 5 secondi. Quindi, sì, thread strettamente su richiesta. Il numero può essere variato usando il setThreads()metodo.

  4. Si assicura che il numero minimo di thread core non sia mai inferiore a uno, altrimenti submit()rifiuterà ogni attività. Poiché i thread core devono essere> = max thread, il metodo setThreads()imposta anche i thread max, sebbene l'impostazione max thread sia inutile per una coda illimitata.


Penso che anche tu debba impostare 'allowCoreThreadTimeOut' su 'true', altrimenti, una volta creati i thread, li manterrai per sempre: gist.github.com/ericdcobb/46b817b384f5ca9d5f5d
eric

oops L'ho perso, scusa, la tua risposta è perfetta allora!
Eric

6

Nel tuo primo esempio, le attività successive vengono rifiutate perché AbortPolicyè l'impostazione predefinita RejectedExecutionHandler. ThreadPoolExecutor contiene i seguenti criteri, che è possibile modificare tramite il setRejectedExecutionHandlermetodo:

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

Sembra che tu voglia un pool di thread nella cache con un CallerRunsPolicy.


5

Nessuna delle risposte qui risolto il mio problema, che aveva a che fare con la creazione di un numero limitato di connessioni HTTP utilizzando il client HTTP di Apache (versione 3.x). Dal momento che mi ci sono volute alcune ore per capire una buona installazione, condividerò:

private ExecutorService executor = new ThreadPoolExecutor(5, 10, 60L,
  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
  Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

Questo crea un ThreadPoolExecutorche inizia con cinque e contiene un massimo di dieci thread in esecuzione simultaneamente usando CallerRunsPolicyper l'esecuzione.


Il problema con questa soluzione è che se aumenti il ​​numero o i produttori, aumenterai il numero di thread che eseguono i thread in background. In molti casi non è quello che vuoi.
Gray,

3

Per Javadoc per ThreadPoolExecutor:

Se ci sono più di corePoolSize ma meno di maximumPoolSize in esecuzione, verrà creato un nuovo thread solo se la coda è piena . Impostando corePoolSize e maximumPoolSize allo stesso modo, si crea un pool di thread di dimensioni fisse.

(Enfasi mia.)

la risposta di jitter è ciò che vuoi, anche se la mia risponde alla tua altra domanda. :)


2

c'è un'altra opzione. Invece di usare la nuova SynchronousQueue puoi usare anche qualsiasi altra coda, ma devi assicurarti che la sua dimensione sia 1, in modo da forzare il servizio di esecuzione per creare un nuovo thread.


Penso che tu intenda la dimensione 0 (per impostazione predefinita), in modo che non ci siano attività in coda e costringi veramente il servizio esecutivo a creare ogni nuovo thread.
Leonmax,

2

Non sembra che nessuna delle risposte risponda effettivamente alla domanda - in effetti non riesco a vedere un modo per farlo - anche se si esegue una sottoclasse da PooledExecutorService poiché molti dei metodi / proprietà sono privati, ad esempio rendendo addIfUnderMaximumPoolSize protetto è possibile eseguire le seguenti operazioni:

class MyThreadPoolService extends ThreadPoolService {
    public void execute(Runnable run) {
        if (poolSize() == 0) {
            if (addIfUnderMaximumPoolSize(run) != null)
                return;
        }
        super.execute(run);
    }
}

Il più vicino che ho avuto è stato questo, ma anche questa non è un'ottima soluzione

new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()) {
    public void execute(Runnable command) {
        if (getPoolSize() == 0 && getActiveCount() < getMaximumPoolSize()) {        
            super.setCorePoolSize(super.getCorePoolSize() + 1);
        }
        super.execute(command);
    }

    protected void afterExecute(Runnable r, Throwable t) {
         // nothing in the queue
         if (getQueue().isEmpty() && getPoolSize() > min) {
             setCorePoolSize(getCorePoolSize() - 1);
         }
    };
 };

ps non testato quanto sopra


2

Ecco un'altra soluzione. Penso che questa soluzione si comporti come vuoi tu (anche se non orgogliosa di questa soluzione):

final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    public boolean offer(Runnable o) {
        if (size() > 1)
            return false;
        return super.offer(o);
    };

    public boolean add(Runnable o) {
        if (super.offer(o))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
};

RejectedExecutionHandler handler = new RejectedExecutionHandler() {         
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        queue.add(r);
    }
};

dbThreadExecutor =
        new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, queue, handler);

2

Questo è quello che vuoi (almeno credo di sì). Per una spiegazione, controlla la risposta di Jonathan Feinberg

Executors.newFixedThreadPool(int n)

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.


4
Certo, potrei usare un pool di thread fisso ma che lascerebbe n thread in eterno per sempre, o fino a quando non chiamo shutdown. Voglio qualcosa di esattamente simile al pool di thread nella cache (crea thread su richiesta e quindi li uccide dopo un certo timeout) ma con un limite al numero di thread che può creare.
Matt Crinklaw-Vogt,

0
  1. Puoi usare ThreadPoolExecutorcome suggerito da @sjlee

    È possibile controllare la dimensione del pool in modo dinamico. Dai un'occhiata a questa domanda per maggiori dettagli:

    Pool di thread dinamici

    O

  2. È possibile utilizzare la nuova API WorkStealingPool , che è stata introdotta con java 8.

    public static ExecutorService newWorkStealingPool()

    Crea un pool di thread che ruba il lavoro utilizzando tutti i processori disponibili come livello di parallelismo di destinazione.

Per impostazione predefinita, il livello di parallelismo è impostato sul numero di core della CPU nel server. Se si dispone di un server CPU a 4 core, la dimensione del pool di thread sarebbe 4. Questa API restituisce il ForkJoinPooltipo di ExecutorService e consente il furto di thread inattivi rubando attività da thread occupati in ForkJoinPool.


0

Il problema è stato riassunto come segue:

Voglio qualcosa di esattamente simile al pool di thread nella cache (crea thread su richiesta e poi li uccide dopo un certo timeout) ma con un limite al numero di thread che può creare e la possibilità di continuare a mettere in coda attività aggiuntive una volta che ha colpito il suo limite di thread.

Prima di indicare la soluzione, spiegherò perché le seguenti soluzioni non funzionano:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

Questo non metterà in coda alcuna attività quando viene raggiunto il limite di 3 perché SynchronousQueue, per definizione, non può contenere alcun elemento.

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

Ciò non creerà più di un singolo thread poiché ThreadPoolExecutor crea thread che superano corePoolSize solo se la coda è piena. Ma LinkedBlockingQueue non è mai pieno.

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

Ciò non riutilizza i thread fino a quando corePoolSize non viene raggiunto perché ThreadPoolExecutor aumenta il numero di thread fino a quando corePoolSize viene raggiunto anche se i thread esistenti sono inattivi. Se riesci a convivere con questo svantaggio, questa è la soluzione più semplice al problema. È anche la soluzione descritta in "Java Concurrency in Practice" (nota in calce a p172).

L'unica soluzione completa al problema descritto sembra essere quella che prevede l'override del offermetodo della coda e la scrittura di un RejectedExecutionHandlercome spiegato nelle risposte a questa domanda: Come ottenere ThreadPoolExecutor per aumentare i thread al massimo prima di mettere in coda?


0

Funziona con Java8 + (e altri, per ora ..)

     Executor executor = new ThreadPoolExecutor(3, 3, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()){{allowCoreThreadTimeOut(true);}};

dove 3 è il limite del numero di thread e 5 è il timeout per i thread inattivi.

Se vuoi verificare se funziona da solo , ecco il codice per fare il lavoro:

public static void main(String[] args) throws InterruptedException {
    final int DESIRED_NUMBER_OF_THREADS=3; // limit of number of Threads for the task at a time
    final int DESIRED_THREAD_IDLE_DEATH_TIMEOUT=5; //any idle Thread ends if it remains idle for X seconds

    System.out.println( java.lang.Thread.activeCount() + " threads");
    Executor executor = new ThreadPoolExecutor(DESIRED_NUMBER_OF_THREADS, DESIRED_NUMBER_OF_THREADS, DESIRED_THREAD_IDLE_DEATH_TIMEOUT, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()) {{allowCoreThreadTimeOut(true);}};

    System.out.println(java.lang.Thread.activeCount() + " threads");

    for (int i = 0; i < 5; i++) {
        final int fi = i;
        executor.execute(() -> waitsout("starting hard thread computation " + fi, "hard thread computation done " + fi,2000));
    }
    System.out.println("If this is UP, it works");

    while (true) {
        System.out.println(
                java.lang.Thread.activeCount() + " threads");
        Thread.sleep(700);
    }

}

static void waitsout(String pre, String post, int timeout) {
    try {
        System.out.println(pre);
        Thread.sleep(timeout);
        System.out.println(post);
    } catch (Exception e) {
    }
}

l'output del codice sopra per me è

1 threads
1 threads
If this is UP, it works
starting hard thread computation 0
4 threads
starting hard thread computation 2
starting hard thread computation 1
4 threads
4 threads
hard thread computation done 2
hard thread computation done 0
hard thread computation done 1
starting hard thread computation 3
starting hard thread computation 4
4 threads
4 threads
4 threads
hard thread computation done 3
hard thread computation done 4
4 threads
4 threads
4 threads
4 threads
3 threads
3 threads
3 threads
1 threads
1 threads
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.