La classe suivante s'enroule autour d'un ThreadPoolExecutor et utilise un sémaphore pour bloquer, puis la file d'attente de travail est pleine:
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);
}
}
Cette classe wrapper est basée sur une solution donnée dans le livre Java Concurrency in Practice de Brian Goetz. La solution dans le livre ne prend que deux paramètres de constructeur: un Executor
et une borne utilisée pour le sémaphore. Ceci est montré dans la réponse donnée par Fixpoint. Il y a un problème avec cette approche: elle peut entrer dans un état où les threads du pool sont occupés, la file d'attente est pleine, mais le sémaphore vient de publier un permis. ( semaphore.release()
dans le bloc finally). Dans cet état, une nouvelle tâche peut récupérer l'autorisation qui vient d'être publiée, mais elle est rejetée car la file d'attente des tâches est pleine. Bien sûr, ce n'est pas quelque chose que vous voulez; vous souhaitez bloquer dans ce cas.
Pour résoudre ce problème, nous devons utiliser une file d'attente illimitée , comme le mentionne clairement JCiP. Le sémaphore agit comme une garde, donnant l'effet d'une taille de file d'attente virtuelle. Cela a pour effet secondaire qu'il est possible que l'unité puisse contenir des maxPoolSize + virtualQueueSize + maxPoolSize
tâches. Pourquoi donc? En raison de la
semaphore.release()
dans le bloc finalement. Si tous les threads du pool appellent cette instruction en même temps, les maxPoolSize
autorisations sont libérées, permettant au même nombre de tâches d'entrer dans l'unité. Si nous utilisions une file d'attente limitée, elle serait toujours pleine, ce qui entraînerait le rejet d'une tâche. Maintenant, comme nous savons que cela ne se produit que lorsqu'un thread de pool est presque terminé, ce n'est pas un problème. Nous savons que le thread du pool ne se bloquera pas, donc une tâche sera bientôt retirée de la file d'attente.
Vous pouvez cependant utiliser une file d'attente limitée. Assurez-vous simplement que sa taille est égale virtualQueueSize + maxPoolSize
. De plus grandes tailles sont inutiles, le sémaphore empêchera de laisser entrer plus d'éléments. Des tailles plus petites entraîneront des tâches rejetées. Le risque que les tâches soient rejetées augmente à mesure que la taille diminue. Par exemple, supposons que vous souhaitiez un exécuteur limité avec maxPoolSize = 2 et virtualQueueSize = 5. Ensuite, prenez un sémaphore avec 5 + 2 = 7 autorisations et une taille de file d'attente réelle de 5 + 2 = 7. Le nombre réel de tâches qui peuvent être dans l'unité est alors 2 + 5 + 2 = 9. Lorsque l'exécuteur est plein (5 tâches en file d'attente, 2 dans le pool de threads, donc 0 permis disponible) et TOUS les threads du pool libèrent leurs autorisations, alors exactement 2 autorisations peuvent être prises par les tâches entrantes.
Maintenant, la solution de JCiP est quelque peu lourde à utiliser car elle n'applique pas toutes ces contraintes (file d'attente illimitée, ou limitée par ces restrictions mathématiques, etc.). Je pense que cela ne sert que de bon exemple pour démontrer comment vous pouvez créer de nouvelles classes thread-safe basées sur les parties déjà disponibles, mais pas en tant que classe réutilisable à part entière. Je ne pense pas que cette dernière était l'intention de l'auteur.