Come ottenere ThreadPoolExecutor per aumentare i thread al massimo prima di accodarsi?


100

Sono stato frustrato per un po 'di tempo con il comportamento predefinito di ThreadPoolExecutorcui supporta i ExecutorServicepool di thread che molti di noi usano. Per citare i Javadoc:

Se sono in esecuzione più thread di corePoolSize ma meno di maximumPoolSize, verrà creato un nuovo thread solo se la coda è piena .

Ciò significa che se si definisce un pool di thread con il codice seguente, non verrà mai avviato il secondo thread perché LinkedBlockingQueueè illimitato.

ExecutorService threadPool =
   new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
      TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));

Solo se si dispone di una coda limitata e la coda è piena vengono avviati i thread sopra il numero di core. Sospetto che un gran numero di programmatori multithread Java junior non sia a conoscenza di questo comportamento di ThreadPoolExecutor.

Ora ho un caso d'uso specifico in cui questo non è ottimale. Sto cercando modi, senza scrivere la mia classe TPE, per aggirare il problema.

I miei requisiti sono per un servizio Web che effettua chiamate a terze parti potenzialmente inaffidabili.

  • Non voglio fare la richiamata in modo sincrono con la richiesta web, quindi voglio usare un pool di thread.
  • Di solito ne ricevo un paio al minuto, quindi non voglio avere un newFixedThreadPool(...)numero elevato di thread che per lo più sono dormienti.
  • Ogni tanto ricevo una raffica di questo traffico e voglio aumentare il numero di thread fino a un valore massimo (diciamo 50).
  • Devo fare un tentativo migliore per eseguire tutti i callback, quindi voglio mettere in coda quelli aggiuntivi sopra i 50. Non voglio sopraffare il resto del mio server web usando un file newCachedThreadPool().

Come posso aggirare questa limitazione in ThreadPoolExecutorcui la coda deve essere delimitata e piena prima che vengano avviati più thread? Come posso fare in modo che avvii più thread prima di accodare le attività?

Modificare:

@Flavio fa un buon punto sull'uso di ThreadPoolExecutor.allowCoreThreadTimeOut(true)per avere il timeout dei thread principali e uscire. L'ho considerato, ma volevo ancora la funzione core-thread. Non volevo che il numero di thread nel pool scendesse al di sotto della dimensione del core, se possibile.


1
Dato che il tuo esempio crea un massimo di 10 thread, c'è qualche reale risparmio nell'usare qualcosa che cresce / si restringe su un pool di thread di dimensioni fisse?
bstempi

Buon punto @bstempi. Il numero era piuttosto arbitrario. L'ho aumentato nella domanda a 50. Non sono proprio sicuro di quanti thread simultanei voglio effettivamente lavorare, ma ora che ho questa soluzione.
Gray

1
Oh mannaggia! 10 voti positivi se potessi qui, esattamente nella stessa posizione in cui mi trovo.
Eugene

Risposte:


51

Come posso aggirare questa limitazione in ThreadPoolExecutorcui la coda deve essere delimitata e piena prima che vengano avviati più thread.

Credo di aver finalmente trovato una soluzione un po 'elegante (forse un po' complicata) a questa limitazione con ThreadPoolExecutor. Coinvolge l'estensione LinkedBlockingQueuedi averlo ritornare falseper queue.offer(...)quando ci sono già alcune operazioni in coda. Se i thread correnti non tengono il passo con le attività in coda, il TPE aggiungerà thread aggiuntivi. Se il pool è già al numero massimo di thread, RejectedExecutionHandlerverrà chiamato. È l'handler che poi entra put(...)in coda.

È certamente strano scrivere una coda in cui offer(...)può tornare falsee put()non si blocca mai, quindi questa è la parte hack. Ma questo funziona bene con l'utilizzo della coda da parte di TPE, quindi non vedo alcun problema nel farlo.

Ecco il codice:

// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    private static final long serialVersionUID = -6903933921423432194L;
    @Override
    public boolean offer(Runnable e) {
        // Offer it to the queue if there is 0 items already queued, else
        // return false so the TPE will add another thread. If we return false
        // and max threads have been reached then the RejectedExecutionHandler
        // will be called which will do the put into the queue.
        if (size() == 0) {
            return super.offer(e);
        } else {
            return false;
        }
    }
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
        60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            // This does the actual put into the queue. Once the max threads
            //  have been reached, the tasks will then queue up.
            executor.getQueue().put(r);
            // we do this after the put() to stop race conditions
            if (executor.isShutdown()) {
                throw new RejectedExecutionException(
                    "Task " + r + " rejected from " + e);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
});

Con questo meccanismo, quando invio le attività alla coda, il ThreadPoolExecutor:

  1. Ridimensiona inizialmente il numero di thread fino alla dimensione del core (qui 1).
  2. Offrilo alla coda. Se la coda è vuota, verrà accodata per essere gestita dai thread esistenti.
  3. Se la coda ha già 1 o più elementi, offer(...)restituirà false.
  4. Se viene restituito false, aumentare il numero di thread nel pool fino a raggiungere il numero massimo (qui 50).
  5. Se al massimo, chiama il file RejectedExecutionHandler
  6. Le RejectedExecutionHandlermette allora il compito nella coda da elaborare dal primo thread disponibili in ordine FIFO.

Sebbene nel mio codice di esempio sopra, la coda sia illimitata, potresti anche definirla come coda limitata. Ad esempio, se aggiungi una capacità di 1000 a, LinkedBlockingQueueallora:

  1. scalare i fili fino a max
  2. quindi mettiti in coda finché non è pieno di 1000 attività
  3. quindi blocca il chiamante fino a quando lo spazio non diventa disponibile per la coda.

Inoltre, se fosse necessario utilizzare offer(...)in, RejectedExecutionHandlerè possibile utilizzare il offer(E, long, TimeUnit)metodo invece con Long.MAX_VALUEcome timeout.

Avvertimento:

Se ti aspetti che le attività vengano aggiunte all'esecutore dopo che è stato chiuso, allora potresti voler essere più intelligente nel buttare RejectedExecutionExceptionfuori dalla nostra abitudine RejectedExecutionHandlerquando il servizio dell'esecutore è stato chiuso. Grazie a @RaduToader per averlo segnalato.

Modificare:

Un'altra modifica a questa risposta potrebbe essere quella di chiedere al TPE se ci sono thread inattivi e accodare l'elemento solo se è così. Dovresti creare una vera classe per questo e aggiungere un ourQueue.setThreadPoolExecutor(tpe);metodo su di essa.

Quindi il tuo offer(...)metodo potrebbe essere simile a:

  1. Controlla per vedere se tpe.getPoolSize() == tpe.getMaximumPoolSize()nel qual caso chiama super.offer(...).
  2. Altrimenti se tpe.getPoolSize() > tpe.getActiveCount()poi chiama super.offer(...)poiché sembrano esserci thread inattivi.
  3. Altrimenti torna falsea forkare un altro thread.

Forse questo:

int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
    return super.offer(e);
} else {
    return false;
}

Si noti che i metodi get su TPE sono costosi poiché accedono ai volatilecampi o (nel caso di getActiveCount()) bloccano il TPE e percorrono l'elenco dei thread. Inoltre, sono presenti condizioni di competizione che possono causare l'accodamento errato di un'attività o il fork di un altro thread quando era presente un thread inattivo.


Ho anche lottato con lo stesso problema, ho finito per ignorare il metodo di esecuzione. Ma questa è davvero una bella soluzione. :)
Batty

Per quanto non mi piaccia l'idea di rompere il contratto Queueper ottenere questo risultato, non sei certo il solo nella tua idea: groovy-programming.com/post/26923146865
bstempi

3
Non hai una stranezza qui in quanto le prime due attività verranno messe in coda e solo dopo che verranno generati nuovi thread? Ad esempio, se il tuo unico thread principale è impegnato con una singola attività di lunga durata e chiami execute(runnable), runnableviene semplicemente aggiunto alla coda. Se chiami execute(secondRunnable), secondRunnableviene aggiunto alla coda. Ma ora se chiami execute(thirdRunnable), thirdRunnableverrà eseguito in un nuovo thread. Le runnableed secondRunnableesegui solo una volta thirdRunnable(o l'attività originale a lunga esecuzione) sono terminate.
Robert Tupelo-Schneck

1
Sì, Robert ha ragione, in un ambiente altamente multithread, la coda a volte cresce mentre ci sono thread liberi da usare. La soluzione sotto la quale estende il TPE - funziona molto meglio. Penso che il suggerimento di Robert dovrebbe essere contrassegnato come risposta, anche se l'hack sopra è interessante
Wanna Know All,

1
Il "RejectedExecutionHandler" ha aiutato l'esecutore all'arresto. Ora sei costretto a usare shutdownNow () poiché shutdown () non impedisce l'aggiunta di nuove attività (a causa di reque)
Radu Toader

28

Impostare la dimensione del core e la dimensione massima sullo stesso valore e consentire la rimozione dei thread principali dal pool con allowCoreThreadTimeOut(true).


+1 Sì, ci avevo pensato, ma volevo comunque avere la funzione core-thread. Non volevo che il pool di thread andasse a 0 thread durante i periodi di inattività. Modificherò la mia domanda per farlo notare. Ma ottimo punto.
Gray

Grazie! È solo il modo più semplice per farlo.
Dmitry Ovchinnikov,

28

Ho già altre due risposte su questa domanda, ma sospetto che questa sia la migliore.

Si basa sulla tecnica della risposta attualmente accettata , ovvero:

  1. Sostituisci il offer()metodo della coda per (a volte) restituire false,
  2. che fa sì che ThreadPoolExecutorspawn un nuovo thread o rifiuti l'attività, e
  3. impostare per RejectedExecutionHandlermettere effettivamente in coda l'attività al rifiuto.

Il problema è quando offer()dovrebbe restituire false. La risposta attualmente accettata restituisce falso quando la coda ha un paio di attività su di essa, ma come ho sottolineato nel mio commento lì, questo causa effetti indesiderati. In alternativa, se restituisci sempre false, continuerai a generare nuovi thread anche quando ci sono thread in attesa in coda.

La soluzione è usare Java 7 LinkedTransferQueuee offer()chiamare tryTransfer(). Quando è presente un thread consumer in attesa, l'attività verrà semplicemente passata a quel thread. Altrimenti, offer()restituirà false e ThreadPoolExecutorgenererà un nuovo thread.

    BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
        @Override
        public boolean offer(Runnable e) {
            return tryTransfer(e);
        }
    };
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
    threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });

Devo essere d'accordo, questo mi sembra più pulito. L'unico svantaggio della soluzione è che LinkedTransferQueue è illimitato, quindi non si ottiene una coda di attività limitata alla capacità senza lavoro aggiuntivo.
Yeroc

Si verifica un problema quando la piscina raggiunge le dimensioni massime. Supponiamo che il pool sia scalato fino alla dimensione massima e che ogni thread stia attualmente eseguendo un'attività, quando il runnable viene inviato questa offerta impl restituirà false e ThreadPoolExecutor tenta di aggiungere il threadWorker, ma il pool ha già raggiunto il massimo, quindi eseguibile verrà semplicemente rifiutato. Come per l'ExceHandler rifiutato che hai scritto, verrà nuovamente offerto in coda, facendo sì che questa danza delle scimmie si ripeta dall'inizio.
Sudheera

1
@Sudheera, credo che ti sbagli. queue.offer(), poiché in realtà sta chiamando LinkedTransferQueue.tryTransfer(), restituirà false e non accoderà l'attività. Tuttavia le RejectedExecutionHandlerchiamate queue.put(), che non falliscono e accodano l'attività.
Robert Tupelo-Schneck

1
@ RobertTupelo-Schneck estremamente utile e simpatico!
Eugene

1
@ RobertTupelo-Schneck Funziona come un fascino! Non so perché non c'è qualcosa di simile fuori dagli schemi in java
Georgi Peev

7

Nota: ora preferisco e consiglio la mia altra risposta .

Ecco una versione che mi sembra molto più semplice: aumenta corePoolSize (fino al limite di maximumPoolSize) ogni volta che viene eseguita una nuova attività, quindi diminuisci corePoolSize (fino al limite della "dimensione del pool di core" specificata dall'utente) ogni volta che un l'attività viene completata.

Per dirla in un altro modo, tieni traccia del numero di attività in esecuzione o accodate e assicurati che corePoolSize sia uguale al numero di attività purché si trovi tra la "dimensione del pool di base" specificata dall'utente e la massimaPoolSize.

public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
    private int userSpecifiedCorePoolSize;
    private int taskCount;

    public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        userSpecifiedCorePoolSize = corePoolSize;
    }

    @Override
    public void execute(Runnable runnable) {
        synchronized (this) {
            taskCount++;
            setCorePoolSizeToTaskCountWithinBounds();
        }
        super.execute(runnable);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
        super.afterExecute(runnable, throwable);
        synchronized (this) {
            taskCount--;
            setCorePoolSizeToTaskCountWithinBounds();
        }
    }

    private void setCorePoolSizeToTaskCountWithinBounds() {
        int threads = taskCount;
        if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
        if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
        setCorePoolSize(threads);
    }
}

Come scritto, la classe non supporta la modifica di corePoolSize o maximumPoolSize specificati dall'utente dopo la costruzione e non supporta la manipolazione della coda di lavoro direttamente o tramite remove()o purge().


Mi piace tranne che per i synchronizedblocchi. Puoi chiamare la coda per ottenere il numero di attività. O forse usi un AtomicInteger?
Grigio

Volevo evitarli, ma il problema è questo. Se ci sono un numero di execute()chiamate in thread separati, ciascuno (1) calcolerà quanti thread sono necessari, (2) setCorePoolSizea quel numero e (3) chiamerà super.execute(). Se i passaggi (1) e (2) non sono sincronizzati, non sono sicuro di come evitare uno sfortunato ordinamento in cui si imposta la dimensione del pool principale su un numero inferiore dopo un numero superiore. Con l'accesso diretto al campo della superclasse, ciò potrebbe essere fatto utilizzando invece il confronto e l'impostazione, ma non vedo un modo pulito per farlo in una sottoclasse senza sincronizzazione.
Robert Tupelo-Schneck

Penso che le penalità per quella condizione di gara siano relativamente basse fintanto che il taskCountcampo è valido (cioè a AtomicInteger). Se due thread ricalcolano la dimensione del pool immediatamente dopo l'altro, dovrebbero ottenere i valori corretti. Se il secondo riduce i thread principali, deve aver visto un calo nella coda o qualcosa del genere.
Grigio

1
Purtroppo penso che sia peggio di così. Supponiamo che le attività 10 e 11 chiamino execute(). Ciascuno chiamerà atomicTaskCount.incrementAndGet()e riceveranno rispettivamente 10 e 11. Ma senza la sincronizzazione (oltre a ottenere il conteggio delle attività e impostare la dimensione del pool principale), è possibile ottenere (1) l'attività 11 imposta la dimensione del pool principale su 11, (2) l'attività 10 imposta la dimensione del pool principale su 10, (3) l'attività 10 chiama super.execute(), (4) l'attività 11 chiama super.execute()e viene accodata.
Robert Tupelo-Schneck

2
Ho testato seriamente questa soluzione ed è chiaramente la migliore. In un ambiente altamente multithread, a volte verrà ancora accodato quando ci sono thread liberi (a causa della natura TPE.execute a thread libero), ma accade raramente, al contrario della soluzione contrassegnata come risposta, dove la race condition ha più possibilità di accade, quindi questo accade più o meno su ogni esecuzione multi-thread.
Voglio sapere tutto

6

Abbiamo una sottoclasse ThreadPoolExecutorche prende un ulteriore creationThresholde sostituisce execute.

public void execute(Runnable command) {
    super.execute(command);
    final int poolSize = getPoolSize();
    if (poolSize < getMaximumPoolSize()) {
        if (getQueue().size() > creationThreshold) {
            synchronized (this) {
                setCorePoolSize(poolSize + 1);
                setCorePoolSize(poolSize);
            }
        }
    }
}

forse anche questo aiuta, ma il tuo sembra più artistico ovviamente ...


Interessante. Grazie per questo. In realtà non sapevo che la dimensione del nucleo fosse mutevole.
Gray

Ora che ci penso ancora, questa soluzione è migliore della mia in termini di controllo delle dimensioni della coda. Ho modificato la mia risposta per fare in modo che il offer(...)metodo restituisca solo in modo falsecondizionale. Grazie!
Gray

4

La risposta consigliata risolve solo uno (1) del problema con il pool di thread JDK:

  1. I pool di thread JDK sono polarizzati verso l'accodamento. Quindi, invece di generare un nuovo thread, metteranno in coda l'attività. Solo se la coda raggiunge il limite, il pool di thread genererà un nuovo thread.

  2. Il ritiro del thread non si verifica quando il carico si alleggerisce. Ad esempio, se abbiamo una raffica di lavori che colpiscono il pool che fa andare il pool al massimo, seguito da un carico leggero di massimo 2 attività alla volta, il pool utilizzerà tutti i thread per servire il carico leggero impedendo il ritiro del thread. (sarebbero necessari solo 2 thread ...)

Insoddisfatto del comportamento di cui sopra, sono andato avanti e ho implementato un pool per superare le carenze di cui sopra.

Per risolvere 2) L'utilizzo della pianificazione Lifo risolve il problema. Questa idea è stata presentata da Ben Maurer alla conferenza applicativa 2015 di ACM: Systems @ Facebook scale

Così è nata una nuova implementazione:

LifoThreadPoolExecutorSQP

Finora questa implementazione migliora le prestazioni di esecuzione asincrona per ZEL .

L'implementazione è in grado di ridurre l'overhead del cambio di contesto, offrendo prestazioni superiori per determinati casi d'uso.

Spero che sia d'aiuto...

PS: JDK Fork Join Pool implementa ExecutorService e funziona come un pool di thread "normale", l'implementazione è performante, utilizza la pianificazione dei thread LIFO, tuttavia non c'è controllo sulla dimensione della coda interna, timeout di ritiro ... e, cosa più importante, le attività non possono essere interrotto durante la cancellazione


1
Peccato che questa implementazione abbia così tante dipendenze esterne. Rendendolo inutile per me: - /
Martin L.

1
È davvero un buon punto (2 °). Sfortunatamente, l'implementazione non è chiara dalle dipendenze esterne, ma può comunque essere adottata se lo desideri.
Alexey Vlasov

1

Nota: ora preferisco e consiglio la mia altra risposta .

Ho un'altra proposta, seguendo l'idea originale di cambiare la coda per restituire false. In questo tutte le attività possono entrare nella coda, ma ogni volta che un'attività viene accodata dopo execute(), la seguiamo con un'attività non operativa sentinella che la coda rifiuta, provocando la generazione di un nuovo thread, che eseguirà il no-op immediatamente seguito da qualcosa dalla coda.

Poiché i thread di lavoro potrebbero eseguire il polling LinkedBlockingQueuedi una nuova attività, è possibile che un'attività venga accodata anche quando è disponibile un thread. Per evitare la generazione di nuovi thread anche quando sono disponibili thread, è necessario tenere traccia di quanti thread sono in attesa di nuove attività sulla coda e generare un nuovo thread solo quando ci sono più attività in coda rispetto a thread in attesa.

final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };

final AtomicInteger waitingThreads = new AtomicInteger(0);

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    @Override
    public boolean offer(Runnable e) {
        // offer returning false will cause the executor to spawn a new thread
        if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
        else return super.offer(e);
    }

    @Override
    public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.poll(timeout, unit);
        } finally {
            waitingThreads.decrementAndGet();
        }
    }

    @Override
    public Runnable take() throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.take();
        } finally {
            waitingThreads.decrementAndGet();
        }
    }
};

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
    @Override
    public void execute(Runnable command) {
        super.execute(command);
        if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
    }
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r == SENTINEL_NO_OP) return;
        else throw new RejectedExecutionException();            
    }
});

0

La migliore soluzione a cui riesco a pensare è estendere.

ThreadPoolExecutoroffre alcuni metodi hook: beforeExecutee afterExecute. Nella tua estensione potresti mantenere l'uso di una coda limitata per alimentare le attività e una seconda coda illimitata per gestire l'overflow. Quando qualcuno chiama submit, puoi provare a inserire la richiesta nella coda delimitata. Se si incontra un'eccezione, è sufficiente inserire l'attività nella coda di overflow. Puoi quindi utilizzare l' afterExecutehook per vedere se c'è qualcosa nella coda di overflow dopo aver terminato un'attività. In questo modo, l'esecutore si prenderà prima cura delle cose nella sua coda delimitata e tirerà automaticamente da questa coda illimitata se il tempo lo consente.

Sembra più lavoro della tua soluzione, ma almeno non implica dare alle code comportamenti inaspettati. Immagino anche che ci sia un modo migliore per controllare lo stato della coda e dei thread piuttosto che fare affidamento sulle eccezioni, che sono piuttosto lente da lanciare.


Non mi piace questa soluzione. Sono abbastanza sicuro che ThreadPoolExecutor non è stato progettato per l'ereditarietà.
scottb

In realtà c'è un esempio di estensione proprio nel JavaDoc. Affermano che la maggior parte probabilmente implementerà solo i metodi hook, ma ti dicono a cos'altro devi prestare attenzione quando estendi.
bstempi

0

Nota: per JDK ThreadPoolExecutor quando si dispone di una coda limitata, si creano nuovi thread solo quando l'offerta restituisce false. Potresti ottenere qualcosa di utile con CallerRunsPolicy che crea un po 'di BackPressure e le chiamate vengono eseguite direttamente nel thread del chiamante.

Ho bisogno che le attività vengano eseguite dai thread creati dal pool e ho una coda ubounded per la pianificazione, mentre il numero di thread all'interno del pool può aumentare o ridursi tra corePoolSize e maximumPoolSize quindi ...

Ho finito per fare un copia incolla completo da ThreadPoolExecutor e cambiare un po 'il metodo di esecuzione perché sfortunatamente non è stato possibile farlo per estensione (chiama metodi privati).

Non volevo generare nuovi thread solo immediatamente quando arriva una nuova richiesta e tutti i thread sono occupati (perché in generale ho attività di breve durata). Ho aggiunto una soglia ma sentiti libero di modificarla in base alle tue esigenze (forse per la maggior parte degli IO è meglio rimuovere questa soglia)

private final AtomicInteger activeWorkers = new AtomicInteger(0);
private volatile double threshold = 0.7d;

protected void beforeExecute(Thread t, Runnable r) {
    activeWorkers.incrementAndGet();
}
protected void afterExecute(Runnable r, Throwable t) {
    activeWorkers.decrementAndGet();
}
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        if (isRunning(c) && this.workQueue.offer(command)) {
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {
                this.addWorker((Runnable) null, false);
            }
            //>>change start
            else if (workerCountOf(recheck) < maximumPoolSize //
                && (activeWorkers.get() > workerCountOf(recheck) * threshold
                    || workQueue.size() > workerCountOf(recheck) * threshold)) {
                this.addWorker((Runnable) null, false);
            }
            //<<change end
        } else if (!this.addWorker(command, false)) {
            this.reject(command);
        }
    }
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.