web-dev-qa-db-fra.com

Impossible de créer un pool de threads mis en cache avec une limite de taille?

Il semble impossible de créer un pool de threads mis en cache avec un nombre de threads limité.

Voici comment la méthode Executors.newCachedThreadPool statique est implémentée dans la bibliothèque Java standard:

 public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Donc, en utilisant ce modèle pour créer un pool de threads en cache de taille fixe:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

Maintenant, si vous utilisez ceci et soumettez 3 tâches, tout ira bien. Soumettre toute autre tâche entraînera des exceptions d'exécution rejetées.

Essayer ceci:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

Tous les threads s'exécuteront de manière séquentielle. En d'autres termes, le pool de threads ne créera jamais plus d'un thread pour gérer vos tâches.

Ceci est un bogue dans la méthode execute de ThreadPoolExecutor? Ou peut-être que c'est intentionnel? Ou il y a une autre manière?

Edit: je veux quelque chose de exactement comme le pool de threads mis en cache (il crée des threads à la demande puis les tue après un certain délai), mais avec une limite du nombre de threads qu'il peut créer et la possibilité de continuer à mettre des tâches supplémentaires en file atteint sa limite de threads. Selon la réponse de SJlee, c'est impossible. En regardant la méthode execute () de ThreadPoolExecutor, il est en effet impossible. J'aurais besoin de sous-classer ThreadPoolExecutor et de remplacer execute () un peu comme SwingWorker, mais ce que SwingWorker fait dans son execute () est un hack complet.

112
Matt Crinklaw-Vogt

ThreadPoolExecutor a plusieurs comportements clés suivants, et vos problèmes peuvent être expliqués par ces comportements.

Quand les tâches sont soumises,

  1. Si le pool de threads n'a pas atteint la taille minimale, il crée de nouveaux threads.
  2. Si la taille du noyau a été atteinte et qu'il n'y a pas de threads inactifs, il met les tâches en file d'attente.
  3. Si la taille minimale a été atteinte, qu'il n'y a pas de threads inactifs et que la file d'attente est saturée, de nouveaux threads sont créés (jusqu'à atteindre la taille maximale).
  4. Si la taille maximale a été atteinte, qu'il n'y a pas de threads inactifs et que la file d'attente est pleine, la stratégie de rejet entre en vigueur.

Dans le premier exemple, notez que la taille de SynchronousQueue est essentiellement égale à 0. Par conséquent, dès que vous atteignez la taille maximale (3), la stratégie de rejet entre en vigueur (# 4).

Dans le deuxième exemple, la file d'attente de choix est une LinkedBlockingQueue qui a une taille illimitée. Par conséquent, vous êtes coincé avec le comportement n ° 2.

Vous ne pouvez pas trop bricoler avec le type en cache ou le type fixe, car leur comportement est presque complètement déterminé.

Si vous souhaitez disposer d'un pool de threads lié et dynamique, vous devez utiliser une taille de noyau positive et une taille maximale associées à une file d'attente d'une taille finie. Par exemple,

new ThreadPoolExecutor(10, // core size
    50, // max size
    10*60, // idle timeout
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(20)); // queue with a size

Addendum: il s’agit d’une réponse assez ancienne et il semble que JDK ait modifié son comportement en ce qui concerne la taille de base de 0. Depuis JDK 1.6, si la taille de base est 0 et que le pool ne contient ThreadPoolExecutor ajoutera un thread pour exécuter cette tâche. Par conséquent, la taille de base de 0 est une exception à la règle ci-dessus. Merci Steve pour apportant à mon attention.

214
sjlee

À moins d'avoir oublié quelque chose, la solution à la question initiale est simple. Le code suivant implémente le comportement souhaité comme décrit par l'affiche d'origine. Il créera jusqu'à 5 threads pour travailler sur une file d'attente sans limite et les threads inactifs se termineront au bout de 60 secondes.

tp = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>());
tp.allowCoreThreadTimeOut(true);
57
user1046052

J'ai eu le même problème. Comme aucune autre réponse ne rassemble toutes les questions, j'ajoute la mienne:

Il est maintenant clairement écrit dans docs : Si vous utilisez une file d 'attente qui ne bloque pas (LinkedBlockingQueue), le paramètre de threads maximum n'a pas d' effet, seuls les threads principaux sont utilisés.

alors:

public class MyExecutor extends ThreadPoolExecutor {

    public MyExecutor() {
        super(4, 4, 5,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        allowCoreThreadTimeOut(true);
    }

    public void setThreads(int n){
        setMaximumPoolSize(Math.max(1, n));
        setCorePoolSize(Math.max(1, n));
    }

}

Cet exécuteur a:

  1. Pas de concept de nombre maximal de threads car nous utilisons une file d'attente sans limite. C'est une bonne chose car une telle file d'attente peut amener l'exécuteur à créer un nombre considérable de threads supplémentaires non essentiels s'il respecte sa politique habituelle.

  2. Une file d'attente de taille maximale Integer.MAX_VALUE. Submit() lancera RejectedExecutionException si le nombre de tâches en attente dépasse Integer.MAX_VALUE. Pas sûr que nous allons commencer par manquer de mémoire ou cela se produira.

  3. A 4 fils de base possibles. Les threads principaux inactifs se ferment automatiquement s'ils sont inactifs pendant 5 secondes.Alors, oui, strictement sur les threads à la demande. Le nombre peut être modifié à l'aide de la méthode setThreads().

  4. S'assure que le nombre minimal de threads principaux n'est jamais inférieur à un, sinon submit() rejettera chaque tâche. Etant donné que les threads principaux doivent être> = max threads, la méthode setThreads() définit également les threads max, bien que le paramètre de thread max soit inutile pour une file d'attente sans limite. 

7
S.D.

Dans votre premier exemple, les tâches suivantes sont rejetées, car AbortPolicy est la valeur par défaut RejectedExecutionHandler. ThreadPoolExecutor contient les stratégies suivantes, que vous pouvez modifier via la méthode setRejectedExecutionHandler:

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

Il semble que vous souhaitiez un pool de threads mis en cache avec CallerRunsPolicy.

6
brianegge

Aucune des réponses ici résolues mon problème, qui avait à faire avec la création d'un nombre limité de connexions HTTP en utilisant le client HTTP d'Apache (version 3.x). Puisqu'il m'a fallu quelques heures pour trouver une bonne configuration, je vais partager:

private ExecutorService executor = new ThreadPoolExecutor(5, 10, 60L,
  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
  Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

Cela crée une ThreadPoolExecutor qui commence par cinq et contient un maximum de dix threads en cours d'exécution simultanément en utilisant CallerRunsPolicy pour l'exécution.

5
paul

Selon la Javadoc pour ThreadPoolExecutor:

S'il y a plus de threads corePoolSize mais inférieurs à maximumPoolSize en cours d'exécution, un nouveau thread sera créé uniquement si la file d'attente est pleine . En définissant corePoolSize et maximumPoolSize de la même manière, vous créez un pool de threads de taille fixe.

(Souligné par moi.)

la réponse de Jitter est ce que vous voulez, bien que la mienne réponde à votre autre question. :)

3
Jonathan Feinberg

il y a une autre option. Au lieu d'utiliser un nouveau SynchronousQueue, vous pouvez également utiliser n'importe quelle autre file d'attente, mais vous devez vous assurer que sa taille est égale à 1, ce qui obligera executorservice à créer un nouveau thread.

2
Ashkrit

C'est ce que vous voulez (du moins, je suppose). Pour une explication, vérifiez Jonathan Feinberg répond

Executors.newFixedThreadPool(int n)

Crée un pool de threads qui réutilise un nombre fixe de threads opérant à partir d'une file d'attente partagée sans limite partagée. À tout moment, les threads nThreads au plus seront des tâches de traitement actives. Si des tâches supplémentaires sont soumises lorsque tous les threads sont actifs, ils attendent dans la file d'attente jusqu'à ce qu'un thread soit disponible. Si un thread se termine en raison d'un échec lors de l'exécution avant l'arrêt, un nouveau prendra sa place si nécessaire pour exécuter les tâches suivantes. Les threads du pool existeront jusqu'à ce qu'il soit explicitement arrêté.

2
jitter

Ne semble pas que l'une des réponses réponde réellement à la question - en fait, je ne vois pas comment le faire - même si vous sous-classez de PooledExecutorService car de nombreuses méthodes/propriétés sont privées, par exemple. Si addIfUnderMaximumPoolSize était protégé, vous pouvez effectuer les opérations suivantes:

class MyThreadPoolService extends ThreadPoolService {
    public void execute(Runnable run) {
        if (poolSize() == 0) {
            if (addIfUnderMaximumPoolSize(run) != null)
                return;
        }
        super.execute(run);
    }
}

Voici ce que j'ai de plus proche - mais même si ce n'est pas une très bonne solution

new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()) {
    public void execute(Runnable command) {
        if (getPoolSize() == 0 && getActiveCount() < getMaximumPoolSize()) {        
            super.setCorePoolSize(super.getCorePoolSize() + 1);
        }
        super.execute(command);
    }

    protected void afterExecute(Runnable r, Throwable t) {
         // nothing in the queue
         if (getQueue().isEmpty() && getPoolSize() > min) {
             setCorePoolSize(getCorePoolSize() - 1);
         }
    };
 };

p.s. pas testé ce qui précède

2
Stuart

Voici une autre solution. Je pense que cette solution se comporte comme vous le souhaitez (mais pas fière de cette solution):

final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    public boolean offer(Runnable o) {
        if (size() > 1)
            return false;
        return super.offer(o);
    };

    public boolean add(Runnable o) {
        if (super.offer(o))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
};

RejectedExecutionHandler handler = new RejectedExecutionHandler() {         
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        queue.add(r);
    }
};

dbThreadExecutor =
        new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, queue, handler);
1
Stuart

Le problème a été résumé comme suit:

Je veux quelque chose de exactement comme le pool de threads mis en cache (il crée des threads à la demande puis les tue après un certain délai), mais avec une limite sur le nombre de threads qu'il peut créer et la possibilité de continuer à mettre en file d'attente des tâches supplémentaires une fois qu'il est arrivé à son terme. limite de fil.

Avant de montrer la solution, je vais expliquer pourquoi les solutions suivantes ne fonctionnent pas:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

Cela ne mettra aucune tâche en file d'attente lorsque la limite de 3 sera atteinte car SynchronousQueue, par définition, ne peut contenir aucun élément.

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

Cela ne créera pas plus d'un seul thread, car ThreadPoolExecutor ne crée des threads dépassant CorePoolSize si la file d'attente est saturée. Mais LinkedBlockingQueue n'est jamais complet.

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

Cela ne réutilisera pas les threads tant que corePoolSize n'est pas atteint, car ThreadPoolExecutor augmente le nombre de threads jusqu'à ce que corePoolSize soit atteint, même si les threads existants sont inactifs. Si vous pouvez vivre avec cet inconvénient, c'est la solution la plus simple au problème. C'est également la solution décrite dans "Java Concurrency in Practice" (note de bas de page p.112).

La seule solution complète au problème décrit semble être celle consistant à redéfinir la méthode offer de la file d'attente et à écrire une RejectedExecutionHandler comme expliqué dans les réponses à cette question: Comment obtenir le ThreadPoolExecutor augmenter le nombre de threads au maximum avant la file d'attente?

0
Stefan Feuerhahn
  1. Vous pouvez utiliser ThreadPoolExecutor comme suggéré par @sjlee

    Vous pouvez contrôler la taille du pool de manière dynamique. Regardez cette question pour plus de détails: 

    Pool de threads dynamique

    OR

  2. Vous pouvez utiliser newWorkStealingPool API, introduit avec Java 8.

    public static ExecutorService newWorkStealingPool()
    

    Crée un pool de threads volant le travail en utilisant tous les processeurs disponibles comme niveau de parallélisme cible.

Par défaut, le niveau de parallélisme est défini sur le nombre de cœurs de processeur de votre serveur. Si vous avez 4 serveurs UC principaux, la taille du pool de threads serait 4. Cette API renvoie le type ForkJoinPool de ExecutorService et autorise le vol de threads inactifs en volant des tâches à des threads occupés de ForkJoinPool. 

0
Ravindra babu