J'essaye d'exécuter beaucoup de tâches en utilisant un ThreadPoolExecutor. Voici un exemple hypothétique:
def workQueue = new ArrayBlockingQueue<Runnable>(3, false)
def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue)
for(int i = 0; i < 100000; i++)
threadPoolExecutor.execute(runnable)
Le problème est que je reçois rapidement une exception Java.util.concurrent.RejectedExecutionException car le nombre de tâches dépasse la taille de la file d'attente de travail. Cependant, le comportement souhaité que je recherche est d'avoir le bloc de thread principal jusqu'à ce qu'il y ait de la place dans la file d'attente. Quelle est la meilleure façon d'y parvenir?
Dans certaines circonstances très limitées, vous pouvez implémenter un Java.util.concurrent.RejectedExecutionHandler qui fait ce dont vous avez besoin.
RejectedExecutionHandler block = new RejectedExecutionHandler() {
rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
executor.getQueue().put( r );
}
};
ThreadPoolExecutor pool = new ...
pool.setRejectedExecutionHandler(block);
À présent. C'est une très mauvaise idée pour les raisons suivantes
Une stratégie presque toujours meilleure consiste à installer ThreadPoolExecutor.CallerRunsPolicy qui étranglera votre application en exécutant la tâche sur le thread qui appelle execute ().
Cependant, il est parfois utile de choisir une stratégie de blocage, avec tous ses risques inhérents. Je dirais dans ces conditions
Donc, comme je le dis. C'est rarement nécessaire et peut être dangereux, mais voilà.
Bonne chance.
Vous pouvez utiliser une variable semaphore
pour empêcher les threads d'entrer dans le pool.
ExecutorService service = new ThreadPoolExecutor(
3,
3,
1,
TimeUnit.HOURS,
new ArrayBlockingQueue<>(6, false)
);
Semaphore lock = new Semaphore(6); // equal to queue capacity
for (int i = 0; i < 100000; i++ ) {
try {
lock.acquire();
service.submit(() -> {
try {
task.run();
} finally {
lock.release();
}
});
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Quelques pièges:
La taille de la file d'attente doit être supérieure au nombre de threads principaux. Si nous devions faire la taille de la file d'attente 3, ce qui arriverait finalement serait:
L'exemple ci-dessus traduit le thread principal bloquant du thread 1. Cela peut sembler une petite période, mais multipliez maintenant la fréquence par des jours et des mois. Tout à coup, de courtes périodes de temps représentent une perte de temps considérable.
Ce que vous devez faire est d’envelopper votre ThreadPoolExecutor dans Executor, ce qui limite explicitement le nombre d’opérations exécutées simultanément:
private static class BlockingExecutor implements Executor {
final Semaphore semaphore;
final Executor delegate;
private BlockingExecutor(final int concurrentTasksLimit, final Executor delegate) {
semaphore = new Semaphore(concurrentTasksLimit);
this.delegate = delegate;
}
@Override
public void execute(final Runnable command) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
final Runnable wrapped = () -> {
try {
command.run();
} finally {
semaphore.release();
}
};
delegate.execute(wrapped);
}
}
Vous pouvez ajuster concurrentTasksLimit à la threadPoolSize + queueSize de votre exécuteur délégué et cela résoudra votre problème
Voici mon extrait de code dans ce cas:
public void executeBlocking( Runnable command ) {
if ( threadPool == null ) {
logger.error( "Thread pool '{}' not initialized.", threadPoolName );
return;
}
ThreadPool threadPoolMonitor = this;
boolean accepted = false;
do {
try {
threadPool.execute( new Runnable() {
@Override
public void run() {
try {
command.run();
}
// to make sure that the monitor is freed on exit
finally {
// Notify all the threads waiting for the resource, if any.
synchronized ( threadPoolMonitor ) {
threadPoolMonitor.notifyAll();
}
}
}
} );
accepted = true;
}
catch ( RejectedExecutionException e ) {
// Thread pool is full
try {
// Block until one of the threads finishes its job and exits.
synchronized ( threadPoolMonitor ) {
threadPoolMonitor.wait();
}
}
catch ( InterruptedException ignored ) {
// return immediately
break;
}
}
} while ( !accepted );
}
threadPool est une instance locale de Java.util.concurrent.ExecutorService déjà initialisée.
C'est ce que j'ai fini par faire:
int NUM_THREADS = 6;
Semaphore lock = new Semaphore(NUM_THREADS);
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 100000; i++) {
try {
lock.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.execute(() -> {
try {
// Task logic
} finally {
lock.release();
}
});
}
J'ai résolu ce problème en utilisant un RejectedExecutionHandler personnalisé, qui bloque simplement le fil d'appel pendant un moment, puis essaie de soumettre à nouveau la tâche:
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);
}
}
Cette classe peut simplement être utilisée dans l'exécuteur du pool de threads en tant que RejectedExecutionHandler comme un autre. Dans cet exemple:
executorPool = new def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue, new BlockWhenQueueFull())
Le seul inconvénient que je vois, c’est que le fil d’appel peut être verrouillé un peu plus longtemps que ce qui est strictement nécessaire (jusqu’à 250 ms). Pour de nombreuses tâches de courte durée, réduisez peut-être le temps d'attente à 10 ms environ. De plus, étant donné que cet exécuteur est effectivement appelé de manière récursive, de très longues attentes avant qu'un thread devienne disponible (en heures) peuvent entraîner un débordement de pile.
Néanmoins, j'aime personnellement cette méthode. Il est compact, facile à comprendre et fonctionne bien. Est-ce que je manque quelque chose d'important?