Comment puis-je contourner cette limitation dans ThreadPoolExecutor
laquelle la file d'attente doit être limitée et pleine avant que d'autres threads ne soient démarrés.
Je crois avoir enfin trouvé une solution un peu élégante (peut-être un peu hacky) à cette limitation avec ThreadPoolExecutor
. Il consiste à étendre LinkedBlockingQueue
l'avoir de retour false
pour queue.offer(...)
quand il y a déjà quelques tâches en attente. Si les threads actuels ne suivent pas les tâches en file d'attente, le TPE ajoutera des threads supplémentaires. Si le pool est déjà à max threads, alors le RejectedExecutionHandler
sera appelé. C'est le gestionnaire qui fait ensuite le put(...)
dans la file d'attente.
Il est certainement étrange d'écrire une file d'attente où offer(...)
peut revenir false
et put()
ne bloque jamais, c'est donc la partie hack. Mais cela fonctionne bien avec l'utilisation de la file d'attente par TPE, donc je ne vois aucun problème à faire cela.
Voici le code:
// 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;
}
}
});
Avec ce mécanisme, lorsque je soumets des tâches à la file d'attente, la ThreadPoolExecutor
volonté:
- Échelle initialement le nombre de threads jusqu'à la taille du noyau (ici 1).
- Offrez-le à la file d'attente. Si la file d'attente est vide, elle sera mise en file d'attente pour être gérée par les threads existants.
- Si la file d'attente contient déjà un ou plusieurs éléments, le
offer(...)
renvoie false.
- Si false est renvoyé, augmentez le nombre de threads dans le pool jusqu'à ce qu'ils atteignent le nombre maximum (ici 50).
- Si au maximum, il appelle le
RejectedExecutionHandler
- Les
RejectedExecutionHandler
met alors la tâche dans la file d' attente à traiter par le premier fil disponible dans l' ordre FIFO.
Bien que dans mon exemple de code ci-dessus, la file d'attente soit illimitée, vous pouvez également la définir comme une file d'attente limitée. Par exemple, si vous ajoutez une capacité de 1000 à la, LinkedBlockingQueue
alors cela:
- mettre à l'échelle les fils au maximum
- puis filez jusqu'à ce qu'il soit plein de 1000 tâches
- puis bloquez l'appelant jusqu'à ce que l'espace soit disponible dans la file d'attente.
De plus, si vous deviez utiliser offer(...)
dans,
RejectedExecutionHandler
vous pouvez utiliser la offer(E, long, TimeUnit)
méthode à la place avec Long.MAX_VALUE
comme délai d'expiration.
Avertissement:
Si vous vous attendez à ce que des tâches soient ajoutées à l'exécuteur après son arrêt, vous voudrez peut-être être plus intelligent en supprimant RejectedExecutionException
notre coutume RejectedExecutionHandler
lorsque le service d'exécuteur a été arrêté. Merci à @RaduToader pour l'avoir signalé.
Éditer:
Une autre modification de cette réponse pourrait être de demander au TPE s'il y a des threads inactifs et de ne mettre l'élément en file d'attente que s'il y en a. Vous devrez créer une vraie classe pour cela et y ajouter une ourQueue.setThreadPoolExecutor(tpe);
méthode.
Ensuite, votre offer(...)
méthode pourrait ressembler à quelque chose comme:
- Vérifiez si le,
tpe.getPoolSize() == tpe.getMaximumPoolSize()
auquel cas il suffit d'appeler super.offer(...)
.
- Sinon,
tpe.getPoolSize() > tpe.getActiveCount()
appelez alors super.offer(...)
car il semble y avoir des threads inactifs.
- Sinon, retournez
false
à la fourchette d'un autre fil.
Peut être ça:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
Notez que les méthodes get sur TPE sont coûteuses car elles accèdent aux volatile
champs ou (dans le cas de getActiveCount()
) verrouillent le TPE et parcourent la liste des threads. En outre, il existe des conditions de concurrence qui peuvent entraîner la mise en file d'attente incorrecte d'une tâche ou la fourche d'un autre thread lorsqu'il y avait un thread inactif.