Come rendere il blocco del metodo submit () di ThreadPoolExecutor se è saturo?


102

Voglio creare un ThreadPoolExecutortale che quando ha raggiunto la sua dimensione massima e la coda è piena, il submit()metodo si blocca quando si tenta di aggiungere nuove attività. Devo implementare un custom RejectedExecutionHandlerper quello o esiste un modo esistente per farlo utilizzando una libreria Java standard?


2
Quello che vuoi è qualcosa di simile al metodo offer () della coda di blocco
extraneon


2
@bacar Non sono d'accordo. Questa domanda e risposta sembra più preziosa (oltre ad essere più vecchia).
JasonMArcher

Risposte:


47

Una delle possibili soluzioni che ho appena trovato:

public class BoundedExecutor {
    private final Executor exec;
    private final Semaphore semaphore;

    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }

    public void submitTask(final Runnable command)
            throws InterruptedException, RejectedExecutionException {
        semaphore.acquire();
        try {
            exec.execute(new Runnable() {
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            semaphore.release();
            throw e;
        }
    }
}

ci sono altre soluzioni? Preferirei qualcosa basato su RejectedExecutionHandlerpoiché sembra un modo standard per gestire tali situazioni.


2
C'è una condizione di gara qui tra il punto in cui il semaforo viene rilasciato nella clausola finalmente e il semaforo viene acquisito?
volni

2
Come accennato in precedenza, questa implementazione è difettosa perché il semaforo viene rilasciato prima del completamento dell'attività. Sarebbe meglio usare il metodo java.util.concurrent.ThreadPoolExecutor # afterExecute (Runnable, Throwable)
FelixM

2
@FelixM: l'utilizzo di java.util.concurrent.ThreadPoolExecutor # afterExecute (Runnable, Throwable) non risolverà il problema perché afterExecute viene chiamato immediatamente dopo task.run () in java.util.concurrent.ThreadPoolExecutor # runWorker (Worker w), prima prendendo l'elemento successivo dalla coda (guardando il codice sorgente di openjdk 1.7.0.6).
Jaan

1
Questa risposta è tratta dal libro Java Concurrency in Practice di Brian Goetz
orangepips

11
Questa risposta non è del tutto corretta, lo sono anche i commenti. Questo pezzo di codice proviene effettivamente da Java Concurrency in Practice ed è corretto se si tiene conto del suo contesto . Il libro afferma chiaramente, letteralmente: "In tale approccio, utilizzare una coda illimitata (...) e impostare il limite sul semaforo in modo che sia uguale alla dimensione del pool più il numero di attività in coda che si desidera consentire". Con una coda illimitata, le attività non verranno mai rifiutate, quindi rilanciare l'eccezione è del tutto inutile! Che è, credo, anche il motivo per cui NONthrow e; è nel libro. JCIP è corretto!
Timmos

30

Puoi usare ThreadPoolExecutor e blockingQueue:

public class ImageManager {
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(blockQueueSize);
    RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    private ExecutorService executorService =  new ThreadPoolExecutor(numOfThread, numOfThread, 
        0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler);

    private int downloadThumbnail(String fileListPath){
        executorService.submit(new yourRunnable());
    }
}

Vorrei solo dire che questa è stata una soluzione follemente veloce e facile da implementare che ha funzionato molto bene!
Ivan

58
Questo esegue le attività rifiutate nel thread di invio. Che funzionalmente non soddisfa i requisiti del PO.
Perception

4
Questo esegue l'attività "nel thread chiamante" invece di bloccarlo per metterlo in coda, il che può avere alcuni effetti negativi, come se più thread lo chiamassero in questo modo, saranno in esecuzione più lavori di "dimensione della coda" e se se l'attività richiede più tempo del previsto, il thread "produttore" potrebbe non tenere occupato l'esecutore. Ma ha funzionato alla grande qui!
Rogerdpack

4
Downvoted: non si blocca quando il TPE è saturo. Questa è solo un'alternativa, non una soluzione.
Timmos,

1
Votato: questo si adatta più o meno al "design di TPE" e "blocca naturalmente" i thread del client per aver dato loro gusti in eccesso da fare. Questo dovrebbe coprire la maggior parte dei casi d'uso, ma non tutti ovviamente e dovresti capire cosa sta succedendo sotto il cofano.
Mike

12

È necessario utilizzare CallerRunsPolicy, che esegue l'attività rifiutata nel thread chiamante. In questo modo, non può inviare nuove attività all'esecutore fino a quando l'attività non viene completata, a quel punto ci saranno alcuni thread del pool liberi o il processo si ripeterà.

http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.CallerRunsPolicy.html

Dai documenti:

Attività rifiutate

Le nuove attività inoltrate nel metodo execute (java.lang.Runnable) verranno rifiutate quando l'Executor è stato chiuso e anche quando l'Executor utilizza limiti finiti sia per i thread massimi che per la capacità della coda di lavoro ed è saturo. In entrambi i casi, il metodo execute richiama il metodo RejectedExecutionHandler.rejectedExecution (java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) del proprio RejectedExecutionHandler. Vengono forniti quattro criteri di gestione predefiniti:

  1. Nel ThreadPoolExecutor.AbortPolicy predefinito, il gestore genera un'eccezione RejectedExecutionException di runtime al rifiuto.
  2. In ThreadPoolExecutor.CallerRunsPolicy, il thread che richiama l'esecuzione stessa esegue l'attività. Ciò fornisce un semplice meccanismo di controllo del feedback che rallenterà la velocità di invio di nuove attività.
  3. In ThreadPoolExecutor.DiscardPolicy, un'attività che non può essere eseguita viene semplicemente eliminata.
  4. In ThreadPoolExecutor.DiscardOldestPolicy, se l'esecutore non viene arrestato, l'attività in testa alla coda di lavoro viene eliminata e quindi viene ritentata l'esecuzione (che può non riuscire di nuovo, causando la ripetizione).

Inoltre, assicurati di usare una coda delimitata, come ArrayBlockingQueue, quando chiami il ThreadPoolExecutorcostruttore. In caso contrario, nulla verrà rifiutato.

Modifica: in risposta al tuo commento, imposta la dimensione di ArrayBlockingQueue in modo che sia uguale alla dimensione massima del pool di thread e utilizza AbortPolicy.

Modifica 2: Ok, vedo cosa stai ottenendo. Che dire di questo: ignorare il beforeExecute()metodo per verificare che getActiveCount()non ecceda getMaximumPoolSize()e, se lo fa, dormire e riprovare?


3
Voglio che il numero di attività eseguite contemporaneamente sia strettamente limitato (dal numero di thread in Executor), questo è il motivo per cui non posso consentire ai thread chiamanti di eseguire queste attività da soli.
Fixpoint

1
AbortPolicy farebbe sì che il thread del chiamante riceva un'eccezione RejectedExecutionException, mentre ho bisogno che si blocchi.
Fixpoint

2
Sto chiedendo il blocco, non la
sospensione

@ Danben: Non intendi CallerRunsPolicy ?
user359996

7
Il problema con CallerRunPolicy è che se si dispone di un produttore di thread singolo, spesso i thread non vengono utilizzati se un'attività a esecuzione prolungata viene rifiutata (perché le altre attività nel pool di thread verranno terminate mentre l'attività a esecuzione prolungata è ancora in esecuzione).
Adam Gent il

6

Hibernate ha un BlockPolicyche è semplice e può fare quello che vuoi:

Vedi: Executors.java

/**
 * A handler for rejected tasks that will have the caller block until
 * space is available.
 */
public static class BlockPolicy implements RejectedExecutionHandler {

    /**
     * Creates a <tt>BlockPolicy</tt>.
     */
    public BlockPolicy() { }

    /**
     * Puts the Runnable to the blocking queue, effectively blocking
     * the delegating thread until space is available.
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try {
            e.getQueue().put( r );
        }
        catch (InterruptedException e1) {
            log.error( "Work discarded, thread was interrupted while waiting for space to schedule: {}", r );
        }
    }
}

4
A pensarci bene, questa è una pessima idea. Non ti consiglio di usarlo. Per buoni motivi, vedi qui: stackoverflow.com/questions/3446011/…
Nate Murray

Inoltre, questo non sta utilizzando la "libreria Java standard", secondo la richiesta dell'OP. Elimina?
user359996

1
Woah, è così brutto. Fondamentalmente questa soluzione interferisce con gli interni del TPE. Il javadoc for ThreadPoolExecutordice letteralmente: "Il metodo getQueue () consente l'accesso alla coda di lavoro per scopi di monitoraggio e debug. L'uso di questo metodo per qualsiasi altro scopo è fortemente sconsigliato.". Che questo sia disponibile in una libreria così ampiamente conosciuta, è assolutamente triste da vedere.
Timmos,

1
com.amazonaws.services.simpleworkflow.flow.worker.BlockCallerPolicy è simile.
Adrian Baker

6

La BoundedExecutorrisposta citata sopra da Java Concurrency in Practice funziona correttamente solo se si utilizza una coda illimitata per l'Executor o il limite del semaforo non è maggiore della dimensione della coda. Il semaforo è lo stato condiviso tra il thread di invio e i thread nel pool, rendendo possibile saturare l'esecutore anche se la dimensione della coda <bound <= (dimensione della coda + dimensione del pool).

L'uso CallerRunsPolicyè valido solo se le tue attività non vengono eseguite per sempre, nel qual caso il thread di invio rimarrà per rejectedExecutionsempre e una cattiva idea se le tue attività impiegano molto tempo per essere eseguite, perché il thread di invio non può inviare nuove attività o fare qualsiasi altra cosa se sta eseguendo un'attività stessa.

Se ciò non è accettabile, suggerisco di controllare la dimensione della coda delimitata dell'esecutore prima di inviare un'attività. Se la coda è piena, attendi qualche istante prima di riprovare. Il throughput ne risentirà, ma suggerisco che sia una soluzione più semplice rispetto a molte delle altre soluzioni proposte e hai la garanzia che nessuna attività verrà rifiutata.


Non sono sicuro di come il controllo della lunghezza della coda prima dell'invio garantisca l'assenza di attività rifiutate in un ambiente multi-thread con più produttori di attività. Non sembra thread-safe.
Tim

5

Lo so, è un hack, ma secondo me l'hack più pulito tra quelli offerti qui ;-)

Poiché ThreadPoolExecutor utilizza la coda di blocco "offer" invece di "put", consente di sovrascrivere il comportamento di "offer" della coda di blocco:

class BlockingQueueHack<T> extends ArrayBlockingQueue<T> {

    BlockingQueueHack(int size) {
        super(size);
    }

    public boolean offer(T task) {
        try {
            this.put(task);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    }
}

ThreadPoolExecutor tp = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new BlockingQueueHack(5));

L'ho provato e sembra funzionare. L'implementazione di una politica di timeout è lasciata all'esercizio del lettore.


Vedi stackoverflow.com/a/4522411/2601671 per una versione pulita di questo. Sono d'accordo, è il modo più pulito per farlo.
Trenton

3

La classe seguente avvolge un ThreadPoolExecutor e utilizza un semaforo per bloccare, quindi la coda di lavoro è piena:

public final class BlockingExecutor { 

    private final Executor executor;
    private final Semaphore semaphore;

    public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
        this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
        this.semaphore = new Semaphore(queueSize + maxPoolSize);
    }

    private void execImpl (final Runnable command) throws InterruptedException {
        semaphore.acquire();
        try {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            // will never be thrown with an unbounded buffer (LinkedBlockingQueue)
            semaphore.release();
            throw e;
        }
    }

    public void execute (Runnable command) throws InterruptedException {
        execImpl(command);
    }
}

Questa classe wrapper si basa su una soluzione fornita nel libro Java Concurrency in Practice di Brian Goetz. La soluzione nel libro accetta solo due parametri del costruttore: un Executore un limite utilizzato per il semaforo. Questo è mostrato nella risposta data da Fixpoint. C'è un problema con questo approccio: può entrare in uno stato in cui i thread del pool sono occupati, la coda è piena, ma il semaforo ha appena rilasciato un permesso. ( semaphore.release()nell'ultimo blocco). In questo stato, una nuova attività può acquisire l'autorizzazione appena rilasciata, ma viene rifiutata perché la coda delle attività è piena. Ovviamente questo non è qualcosa che vuoi; si desidera bloccare in questo caso.

Per risolvere questo problema, dobbiamo utilizzare una coda illimitata , come menziona chiaramente JCiP. Il semaforo funge da guardia, dando l'effetto di una dimensione di coda virtuale. Ciò ha l'effetto collaterale che è possibile che l'unità possa contenere maxPoolSize + virtualQueueSize + maxPoolSizeattività. Perché? A causa del semaphore.release()blocco finale. Se tutti i thread del pool chiamano questa istruzione contemporaneamente, maxPoolSizevengono rilasciati i permessi, consentendo allo stesso numero di attività di entrare nell'unità. Se stessimo utilizzando una coda delimitata, sarebbe ancora piena, risultando in un'attività rifiutata. Ora, poiché sappiamo che ciò si verifica solo quando un thread del pool è quasi finito, questo non è un problema. Sappiamo che il thread del pool non si bloccherà, quindi un'attività verrà presto prelevata dalla coda.

Tuttavia, puoi utilizzare una coda limitata. Assicurati solo che le sue dimensioni siano uguali virtualQueueSize + maxPoolSize. Dimensioni maggiori sono inutili, il semaforo impedirà di far entrare più elementi. Dimensioni più piccole comporteranno attività rifiutate. La possibilità che le attività vengano rifiutate aumenta al diminuire delle dimensioni. Ad esempio, supponi di volere un esecutore delimitato con maxPoolSize = 2 e virtualQueueSize = 5. Quindi prendi un semaforo con 5 + 2 = 7 permessi e una dimensione della coda effettiva di 5 + 2 = 7. Il numero reale di attività che possono essere presenti nell'unità è quindi 2 + 5 + 2 = 9. Quando l'esecutore è pieno (5 attività in coda, 2 nel pool di thread, quindi 0 permessi disponibili) e TUTTI i thread del pool rilasciano i loro permessi, allora esattamente 2 permessi possono essere presi dalle attività in arrivo.

Ora la soluzione di JCiP è un po 'complicata da usare in quanto non applica tutti questi vincoli (coda illimitata o limitata da quelle restrizioni matematiche, ecc.). Penso che questo serva solo come un buon esempio per dimostrare come creare nuove classi thread-safe basate sulle parti già disponibili, ma non come una classe completa e riutilizzabile. Non credo che quest'ultima fosse l'intenzione dell'autore.


2

puoi usare un RejectedExecutionHandler personalizzato come questo

ThreadPoolExecutor tp= new ThreadPoolExecutor(core_size, // core size
                max_handlers, // max size 
                timeout_in_seconds, // idle timeout 
                TimeUnit.SECONDS, queue, new RejectedExecutionHandler() {
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        // This will block if the queue is full
                        try {
                            executor.getQueue().put(r);
                        } catch (InterruptedException e) {
                            System.err.println(e.getMessage());
                        }

                    }
                });

1
La documentazione per getQueue () menziona esplicitamente che l' accesso alla coda delle attività è inteso principalmente per il debug e il monitoraggio.
Chadi

0

Crea la tua coda di blocco che deve essere utilizzata dall'Executor, con il comportamento di blocco che stai cercando, restituendo sempre la capacità rimanente disponibile (assicurandoti che l'esecutore non proverà a creare più thread del suo pool principale o attiverà il gestore di rifiuto).

Credo che questo ti darà il comportamento di blocco che stai cercando. Un gestore del rifiuto non si adatterà mai al conto, poiché ciò indica che l'esecutore non può eseguire l'attività. Quello che potrei immaginare è che si ottiene una qualche forma di "attesa occupata" nel conduttore. Non è quello che vuoi, vuoi una coda per l'esecutore che blocchi il chiamante ...


2
ThreadPoolExecutorusa il offermetodo per aggiungere attività alla coda. Se creassi un costume BlockingQueueche si blocca offer, si romperebbe BlockingQueueil contratto.
Fixpoint

@Shooshpanchick, ciò romperebbe il contratto di BlockingQueues. e allora? se sei così appassionato puoi abilitare esplicitamente il comportamento solo durante submit () (ci vorrà un ThreadLocal)
bestsss

Vedi anche questa risposta a un'altra domanda che spiega questa alternativa.
Robert Tupelo-Schneck

c'è un motivo per cui è ThreadPoolExecutorstato implementato per utilizzare offere non put(la versione di blocco)? Inoltre, se ci fosse un modo per il codice client di indicare quale usare e quando, molte persone che tentavano di eseguire manualmente soluzioni personalizzate sarebbero state sollevate
asgs

0

Per evitare problemi con la soluzione @FixPoint. Si potrebbe usare ListeningExecutorService e rilasciare il semaforo onSuccess e onFailure all'interno di FutureCallback.


Ciò ha gli stessi problemi intrinseci del semplice wrapping Runnablepoiché quei metodi vengono ancora chiamati prima della pulizia del lavoratore nella normalità ThreadPoolExecutor. Ovvero, dovrai comunque gestire le eccezioni di rifiuto.
Adam Gent il

0

Recentemente ho riscontrato che questa domanda ha lo stesso problema. L'OP non lo dice esplicitamente, ma non vogliamo usare il RejectedExecutionHandlerquale esegue un'attività sul thread del mittente, perché questo sottoutilizzerà i thread di lavoro se questa attività è di lunga durata.

Leggendo tutte le risposte e commenti, in particolare la soluzione viziata con il semaforo o utilizzando afterExecuteho dato uno sguardo più da vicino al codice del ThreadPoolExecutor per vedere se c'è qualche via d'uscita. Sono rimasto stupito nel vedere che ci sono più di 2000 righe di codice (commentato), alcune delle quali mi fanno venire le vertigini . Dato il requisito piuttosto semplice che ho effettivamente --- un produttore, diversi consumatori, lascia che il produttore blocchi quando nessun consumatore può accettare il lavoro --- ho deciso di lanciare la mia soluzione. Non è un ExecutorServicema solo un Executor. E non adatta il numero di thread al carico di lavoro, ma contiene solo un numero fisso di thread, che si adatta anche alle mie esigenze. Ecco il codice. Sentiti libero di lamentarti :-)

package x;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;

/**
 * distributes {@code Runnable}s to a fixed number of threads. To keep the
 * code lean, this is not an {@code ExecutorService}. In particular there is
 * only very simple support to shut this executor down.
 */
public class ParallelExecutor implements Executor {
  // other bounded queues work as well and are useful to buffer peak loads
  private final BlockingQueue<Runnable> workQueue =
      new SynchronousQueue<Runnable>();
  private final Thread[] threads;

  /*+**********************************************************************/
  /**
   * creates the requested number of threads and starts them to wait for
   * incoming work
   */
  public ParallelExecutor(int numThreads) {
    this.threads = new Thread[numThreads];
    for(int i=0; i<numThreads; i++) {
      // could reuse the same Runner all over, but keep it simple
      Thread t = new Thread(new Runner());
      this.threads[i] = t;
      t.start();
    }
  }
  /*+**********************************************************************/
  /**
   * returns immediately without waiting for the task to be finished, but may
   * block if all worker threads are busy.
   * 
   * @throws RejectedExecutionException if we got interrupted while waiting
   *         for a free worker
   */
  @Override
  public void execute(Runnable task)  {
    try {
      workQueue.put(task);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new RejectedExecutionException("interrupt while waiting for a free "
          + "worker.", e);
    }
  }
  /*+**********************************************************************/
  /**
   * Interrupts all workers and joins them. Tasks susceptible to an interrupt
   * will preempt their work. Blocks until the last thread surrendered.
   */
  public void interruptAndJoinAll() throws InterruptedException {
    for(Thread t : threads) {
      t.interrupt();
    }
    for(Thread t : threads) {
      t.join();
    }
  }
  /*+**********************************************************************/
  private final class Runner implements Runnable {
    @Override
    public void run() {
      while (!Thread.currentThread().isInterrupted()) {
        Runnable task;
        try {
          task = workQueue.take();
        } catch (InterruptedException e) {
          // canonical handling despite exiting right away
          Thread.currentThread().interrupt(); 
          return;
        }
        try {
          task.run();
        } catch (RuntimeException e) {
          // production code to use a logging framework
          e.printStackTrace();
        }
      }
    }
  }
}

0

Credo che ci sia un modo abbastanza elegante per risolvere questo problema utilizzando java.util.concurrent.Semaphoree delegando il comportamento di Executor.newFixedThreadPool. Il nuovo servizio esecutore eseguirà una nuova attività solo quando è presente un thread per farlo. Il blocco è gestito da Semaphore con numero di permessi pari al numero di thread. Quando un'attività è terminata, restituisce un permesso.

public class FixedThreadBlockingExecutorService extends AbstractExecutorService {

private final ExecutorService executor;
private final Semaphore blockExecution;

public FixedThreadBlockingExecutorService(int nTreads) {
    this.executor = Executors.newFixedThreadPool(nTreads);
    blockExecution = new Semaphore(nTreads);
}

@Override
public void shutdown() {
    executor.shutdown();
}

@Override
public List<Runnable> shutdownNow() {
    return executor.shutdownNow();
}

@Override
public boolean isShutdown() {
    return executor.isShutdown();
}

@Override
public boolean isTerminated() {
    return executor.isTerminated();
}

@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
    return executor.awaitTermination(timeout, unit);
}

@Override
public void execute(Runnable command) {
    blockExecution.acquireUninterruptibly();
    executor.execute(() -> {
        try {
            command.run();
        } finally {
            blockExecution.release();
        }
    });
}

Ho implementato il BoundedExecutor descritto in Java Concurrency in Practice e ho capito che il semaforo deve essere inizializzato con il flag di equità impostato su true per garantire che i permessi del semaforo siano offerti nell'ordine in cui vengono effettuate le richieste. Fare riferimento docs.oracle.com/javase/7/docs/api/java/util/concurrent/… . per i dettagli
Prahalad Deshpande

0

In passato avevo la stessa esigenza: una sorta di coda di blocco con una dimensione fissa per ogni client supportata da un pool di thread condiviso. Ho finito per scrivere il mio tipo di ThreadPoolExecutor:

UserThreadPoolExecutor (coda di blocco (per client) + threadpool (condiviso tra tutti i client))

Vedi: https://github.com/d4rxh4wx/UserThreadPoolExecutor

A ogni UserThreadPoolExecutor viene assegnato un numero massimo di thread da un ThreadPoolExecutor condiviso

Ogni UserThreadPoolExecutor può:

  • invia un'attività all'esecutore del pool di thread condiviso se la sua quota non viene raggiunta. Se viene raggiunta la sua quota, il lavoro viene messo in coda (blocco non consumante in attesa della CPU). Una volta completata una delle attività inviate, la quota viene ridotta, consentendo di inviare un'altra attività in attesa a ThreadPoolExecutor
  • attendere il completamento delle attività rimanenti

0

Ho trovato questa politica di rifiuto nel client di ricerca elastico. Blocca il thread del chiamante sulla coda di blocco. Codice sotto-

 static class ForceQueuePolicy implements XRejectedExecutionHandler 
 {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 
        {
            try 
            {
                executor.getQueue().put(r);
            } 
            catch (InterruptedException e) 
            {
                //should never happen since we never wait
                throw new EsRejectedExecutionException(e);
            }
        }

        @Override
        public long rejected() 
        {
            return 0;
        }
}

0

Recentemente avevo bisogno di realizzare qualcosa di simile, ma su un file ScheduledExecutorService.

Ho dovuto anche assicurarmi di gestire il ritardo trasmesso al metodo e assicurarmi che l'attività venga inviata per essere eseguita nel momento in cui il chiamante si aspetta o semplicemente fallisce lanciando un file RejectedExecutionException.

Altri metodi ScheduledThreadPoolExecutorper eseguire o inviare un'attività internamente chiamano #scheduleche a loro volta richiameranno i metodi sovrascritti.

import java.util.concurrent.*;

public class BlockingScheduler extends ScheduledThreadPoolExecutor {
    private final Semaphore maxQueueSize;

    public BlockingScheduler(int corePoolSize,
                             ThreadFactory threadFactory,
                             int maxQueueSize) {
        super(corePoolSize, threadFactory, new AbortPolicy());
        this.maxQueueSize = new Semaphore(maxQueueSize);
    }

    @Override
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(delay));
        return super.schedule(command, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(callable, unit.toMillis(delay));
        return super.schedule(callable, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleAtFixedRate(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long period,
                                                     TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleWithFixedDelay(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable t) {
        super.afterExecute(runnable, t);
        try {
            if (t == null && runnable instanceof Future<?>) {
                try {
                    ((Future<?>) runnable).get();
                } catch (CancellationException | ExecutionException e) {
                    t = e;
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // ignore/reset
                }
            }
            if (t != null) {
                System.err.println(t);
            }
        } finally {
            releaseQueueUsage();
        }
    }

    private long beforeSchedule(Runnable runnable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(runnable, this);
            return 0;
        }
    }

    private long beforeSchedule(Callable callable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(new FutureTask(callable), this);
            return 0;
        }
    }

    private long getQueuePermitAndModifiedDelay(long delay) throws InterruptedException {
        final long beforeAcquireTimeStamp = System.currentTimeMillis();
        maxQueueSize.tryAcquire(delay, TimeUnit.MILLISECONDS);
        final long afterAcquireTimeStamp = System.currentTimeMillis();
        return afterAcquireTimeStamp - beforeAcquireTimeStamp;
    }

    private void releaseQueueUsage() {
        maxQueueSize.release();
    }
}

Ho il codice qui, apprezzerò qualsiasi feedback. https://github.com/AmitabhAwasthi/BlockingScheduler


Questa risposta si basa completamente sul contenuto dei link esterni. Se mai dovessero diventare invalidi, la tua risposta sarebbe inutile. Quindi, modifica la tua risposta e aggiungi almeno un riepilogo di ciò che può essere trovato lì. Grazie!
Fabio dice Reinstate Monica

@fabio: grazie per avermelo fatto notare. Ho aggiunto il codice in modo che ora abbia più senso per i lettori. Apprezzo il tuo commento :)
Dev Amitabh


0

Non mi piace sempre CallerRunsPolicy, soprattutto perché consente all'attività rifiutata di "saltare la coda" e di essere eseguita prima delle attività inviate in precedenza. Inoltre, l'esecuzione dell'attività sul thread chiamante potrebbe richiedere molto più tempo dell'attesa che il primo slot diventi disponibile.

Ho risolto questo problema utilizzando un RejectedExecutionHandler personalizzato, che blocca semplicemente il thread chiamante per un po 'e quindi prova a inviare nuovamente l'attività:

public class BlockWhenQueueFull implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // The pool is full. Wait, then try again.
        try {
            long waitMs = 250;
            Thread.sleep(waitMs);
        } catch (InterruptedException interruptedException) {}

        executor.execute(r);
    }
}

Questa classe può essere utilizzata solo nell'esecutore del pool di thread come RejectedExecutinHandler come qualsiasi altra, ad esempio:

executorPool = new ThreadPoolExecutor(1, 1, 10,
                                      TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
                                      new BlockWhenQueueFull());

L'unico svantaggio che vedo è che il thread chiamante potrebbe rimanere bloccato un po 'più a lungo del necessario (fino a 250 ms). Inoltre, poiché questo esecutore viene effettivamente chiamato in modo ricorsivo, attese molto lunghe prima che un thread diventi disponibile (ore) potrebbero causare un overflow dello stack.

Tuttavia, personalmente mi piace questo metodo. È compatto, facile da capire e funziona bene.


1
Come dici tu stesso: questo può creare uno stackoverflow. Non qualcosa che vorrei avere nel codice di produzione.
Harald

Ognuno dovrebbe prendere le proprie decisioni. Per il mio carico di lavoro, questo non è un problema. Le attività vengono eseguite in pochi secondi invece delle ore che sarebbero necessarie per far saltare lo stack. Inoltre, lo stesso si può dire per virtualmente qualsiasi algoritmo ricorsivo. È un motivo per non utilizzare mai alcun algoritmo ricorsivo in produzione?
TinkerTank
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.