Explication détaillée du pool de threads Java, c'est peut-être le meilleur article

Dans l'article précédent, quand on utilise des threads, on crée un thread, qui est très simple à mettre en place, mais il y a un problème :

S'il existe un grand nombre de threads simultanés et que chaque thread se termine par l'exécution d'une tâche pendant une courte période, la création fréquente de threads réduira considérablement l'efficacité du système, car il faut du temps pour créer et détruire fréquemment des threads.

Existe-t-il donc un moyen de rendre les threads réutilisables, c'est-à-dire qu'après l'exécution d'une tâche, elle ne sera pas détruite, mais pourra continuer à exécuter d'autres tâches ?

En Java, cet effet peut être obtenu via un pool de threads. Aujourd'hui, nous allons expliquer en détail le pool de threads de Java. Nous commencerons par les méthodes de la classe principale ThreadPoolExecutor, puis décrirons son principe d'implémentation, puis donnerons un exemple de son utilisation, et enfin discuterons de la façon de le configurer raisonnablement. taille du pool de threads.

1. La classe ThreadPoolExecutor en Java

La classe java.uitl.concurrent.ThreadPoolExecutor est la classe principale du pool de threads. Par conséquent, si vous souhaitez bien comprendre le pool de threads en Java, vous devez d'abord comprendre cette classe. Examinons le code source d'implémentation spécifique de la classe ThreadPoolExecutor.

Quatre constructeurs sont fournis dans la classe ThreadPoolExecutor :

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

Comme on peut le voir dans le code ci-dessus, ThreadPoolExecutor hérite de la classe AbstractExecutorService et fournit quatre constructeurs. En fait, en observant l'implémentation spécifique du code source de chaque constructeur, on constate que les trois premiers constructeurs sont le quatrième constructeur appelé travail d'initialisation effectuée par l'appareil.

Ce qui suit explique la signification de chaque paramètre dans le constructeur :

  • corePoolSize : La taille du pool de base, ce paramètre a une grande relation avec le principe de mise en œuvre du pool de threads décrit plus loin. Une fois le pool de threads créé, par défaut, il n'y a pas de threads dans le pool de threads, mais attendez qu'une tâche arrive avant de créer un thread pour exécuter la tâche, sauf si la méthode prestartAllCoreThreads() ou prestartCoreThread() est appelée, à partir de ces deux méthodes Comme vous pouvez le voir d'après le nom, cela signifie pré-créer des threads, c'est-à-dire créer des threads corePoolSize ou un thread avant qu'aucune tâche n'arrive. Par défaut, après la création du pool de threads, le nombre de threads dans le pool de threads est 0. Lorsqu'une tâche arrive, un thread est créé pour exécuter la tâche. Lorsque le nombre de threads dans le pool de threads atteint corePoolSize, il être envoyé à Les tâches sont placées dans la file d'attente du cache ;
  • maximumPoolSize : Le nombre maximum de threads dans le pool de threads, ce paramètre est également un paramètre très important, il indique combien de threads peuvent être créés dans le pool de threads au maximum ;
  • keepAliveTime : indique combien de temps le thread sera terminé lorsqu'il n'y a pas d'exécution de tâche. Par défaut, keepAliveTime ne fonctionnera que lorsque le nombre de threads dans le pool de threads est supérieur à corePoolSize, jusqu'à ce que le nombre de threads dans le pool de threads ne soit pas supérieur à corePoolSize, c'est-à-dire lorsque le nombre de threads dans le pool de threads est supérieur que corePoolSize, si un thread est inactif Lorsque le temps atteint keepAliveTime, il se terminera jusqu'à ce que le nombre de threads dans le pool de threads ne dépasse pas corePoolSize. Cependant, si la méthode allowCoreThreadTimeOut(boolean) est appelée, lorsque le nombre de threads dans le pool de threads n'est pas supérieur à corePoolSize, le paramètre keepAliveTime fonctionnera également jusqu'à ce que le nombre de threads dans le pool de threads soit égal à 0 ;
  • unit : L'unité de temps du paramètre keepAliveTime, il y a 7 valeurs, et il y a 7 propriétés statiques dans la classe TimeUnit :
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue : une file d'attente de blocage utilisée pour stocker les tâches en attente d'exécution. Le choix de ce paramètre est également très important et aura un impact significatif sur le processus d'exécution du pool de threads. De manière générale, la file d'attente de blocage a ici les options suivantes :
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

ArrayBlockingQueue et PriorityBlockingQueue sont moins utilisés, et LinkedBlockingQueue et Synchronous sont généralement utilisés. La stratégie de mise en file d'attente du pool de threads est liée à BlockingQueue.

  • threadFactory : usine de threads, principalement utilisée pour créer des threads ;
  • handler : Indique la stratégie en cas de refus de traitement d'une tâche, avec les quatre valeurs suivantes :
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

 La relation entre la configuration de paramètres spécifiques et le pool de threads sera décrite dans la section suivante.

D'après le code de la classe ThreadPoolExecutor donné ci-dessus, nous pouvons savoir que ThreadPoolExecutor hérite de AbstractExecutorService. Examinons l'implémentation de AbstractExecutorService :

public abstract class AbstractExecutorService implements ExecutorService {
 
     
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
    public Future<?> submit(Runnable task) {};
    public <T> Future<T> submit(Runnable task, T result) { };
    public <T> Future<T> submit(Callable<T> task) { };
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                            boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                           long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
    };
}

AbstractExecutorService est une classe abstraite qui implémente l'interface ExecutorService.

Regardons l'implémentation de l'interface ExecutorService :

public interface ExecutorService extends Executor {
 
    void shutdown();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Et ExecutorService hérite de l'interface Executor. Examinons l'implémentation de l'interface Executor :

public interface Executor {
    void execute(Runnable command);
}

À ce stade, vous devez comprendre la relation entre ThreadPoolExecutor, AbstractExecutorService, ExecutorService et Executor.

Executor est une interface de niveau supérieur, dans laquelle une seule méthode execute(Runnable) est déclarée, la valeur de retour est void et le paramètre est de type Runnable. On peut comprendre d'après le sens littéral qu'il est utilisé pour exécuter les tâches passé;

Ensuite, l'interface ExecutorService hérite de l'interface Executor et déclare certaines méthodes : submit, invocAll, invocAny, shutdown, etc. ;

La classe abstraite AbstractExecutorService implémente l'interface ExecutorService et implémente essentiellement toutes les méthodes déclarées dans ExecutorService ;

Ensuite, ThreadPoolExecutor hérite de la classe AbstractExecutorService.

Il existe plusieurs méthodes très importantes dans la classe ThreadPoolExecutor :

execute()
submit()
shutdown()
shutdownNow()

La méthode execute() est en fait une méthode déclarée dans Executor, qui est spécifiquement implémentée dans ThreadPoolExecutor. Cette méthode est la méthode principale de ThreadPoolExecutor. Grâce à cette méthode, une tâche peut être soumise au pool de threads pour être exécutée par le pool de threads.

La méthode submit() est une méthode déclarée dans ExecutorService. Elle a été implémentée dans AbstractExecutorService. Elle n'est pas réécrite dans ThreadPoolExecutor. Cette méthode est également utilisée pour soumettre des tâches au pool de threads, mais elle est différente de la méthode execute() ). différent, il peut retourner le résultat de l'exécution de la tâche, regardez l'implémentation de la méthode submit(), vous constaterez qu'il appelle en fait la méthode execute(), mais il utilise le Future pour obtenir le résultat de l'exécution de la tâche (Future related le contenu sera décrit dans le prochain article).

shutdown() et shutdownNow() sont utilisés pour arrêter le pool de threads.

Il existe de nombreuses autres façons :

Par exemple : getQueue(), getPoolSize(), getActiveCount(), getCompletedTaskCount() et d'autres méthodes pour obtenir les propriétés liées aux pools de threads. Les amis intéressés peuvent vérifier l'API par eux-mêmes.

Deuxièmement, une analyse approfondie du principe de mise en œuvre du pool de threads

Dans la section précédente, nous avons présenté ThreadPoolExecutor d'un point de vue macro. Analysons en profondeur le principe d'implémentation spécifique du pool de threads. Nous l'expliquerons sous les aspects suivants :

1. État du pool de threads

Une variable volatile est définie dans ThreadPoolExecutor et plusieurs variables finales statiques sont définies pour représenter les différents états du pool de threads :

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState représente l'état du pool de threads actuel, qui est une variable volatile utilisée pour assurer la visibilité entre les threads ;

Les variables finales statiques suivantes représentent plusieurs valeurs possibles de runState.

Lorsque le pool de threads est créé, initialement, le pool de threads est dans l'état RUNNING ;

Si la méthode shutdown() est appelée, le pool de threads est dans l'état SHUTDOWN. À ce stade, le pool de threads ne peut pas accepter de nouvelles tâches et attendra que toutes les tâches soient exécutées ;

Si la méthode shutdownNow() est appelée, le pool de threads est à l'état STOP. À ce stade, le pool de threads ne peut pas accepter de nouvelles tâches et essaiera de mettre fin aux tâches en cours d'exécution ;

Lorsque le pool de threads est à l'état SHUTDOWN ou STOP et que tous les threads de travail ont été détruits, que la file d'attente du cache des tâches a été vidée ou que l'exécution est terminée, le pool de threads est défini sur l'état TERMINATED.

2. Exécution des tâches

Avant de comprendre l'ensemble du processus de soumission d'une tâche au pool de threads jusqu'à l'achèvement de l'exécution de la tâche, examinons quelques autres variables membres importantes dans la classe ThreadPoolExecutor :

private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小
                                                              //、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集
 
private volatile long  keepAliveTime;    //线程存货时间   
private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数
 
private volatile int   poolSize;       //线程池中当前的线程数
 
private volatile RejectedExecutionHandler handler; //任务拒绝策略
 
private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程
 
private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数
 
private long completedTaskCount;   //用来记录已经执行完毕的任务个数

Le rôle de chaque variable a été balisé, ici pour se concentrer sur l'explication des trois variables corePoolSize, maximumPoolSize, largePoolSize.

corePoolSize est traduit dans la taille du pool de base dans de nombreux endroits.En fait, ma compréhension est la taille du pool de threads. Prenons un exemple simple :

S'il y a une usine, il y a 10 ouvriers dans l'usine et chaque ouvrier ne peut effectuer qu'une seule tâche à la fois.

Par conséquent, tant qu'un des 10 ouvriers est inactif, la tâche sera assignée à l'ouvrier inactif ;

Lorsque 10 travailleurs ont des tâches à accomplir, s'il reste une tâche, la tâche sera mise en file d'attente et attendue ;

Si le nombre de nouvelles tâches augmente beaucoup plus rapidement que la vitesse à laquelle les travailleurs effectuent les tâches, le superviseur de l'usine peut vouloir prendre des mesures correctives, telles que le recrutement de 4 travailleurs temporaires ;

Puis les tâches sont également confiées à ces 4 intérimaires ;

Si la vitesse de 14 travailleurs n'est toujours pas suffisante, le superviseur de l'usine peut envisager de ne pas accepter de nouvelles tâches ou d'abandonner certaines tâches précédentes.

Lorsque certains des travailleurs 14 sont libres et que le taux de croissance des nouvelles tâches est relativement lent, le superviseur de l'usine peut envisager de quitter les travailleurs temporaires 4 et de ne garder que les travailleurs 10. Après tout, il en coûte de l'argent pour embaucher des travailleurs supplémentaires.

Le corePoolSize dans cet exemple est 10 et le maximumPoolSize est 14 (10+4).

En d'autres termes, corePoolSize est la taille du pool de threads, et maximumPoolSize est un remède pour le pool de threads à mon avis, c'est-à-dire un remède lorsque le nombre de tâches devient soudainement trop important.

Toutefois, pour faciliter la compréhension, corePoolSize est traduit en taille de pool principal plus loin dans cet article.

largePoolSize est juste une variable utilisée pour l'enregistrement, qui est utilisée pour enregistrer le plus grand nombre de threads qui aient jamais existé dans le pool de threads, et n'a rien à voir avec la capacité du pool de threads.

Venons-en maintenant au fait et voyons par quel processus la tâche est passée de la soumission à l'exécution finale.

Dans la classe ThreadPoolExecutor, la principale méthode de soumission de tâche est la méthode execute(). Bien que les tâches puissent également être soumises via submit, en fait, la méthode execute() est en fait appelée dans la méthode submit, nous n'avons donc qu'à étudier la méthode execute () Le principe de réalisation peut être :

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);
        }
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); // is shutdown or saturated
    }
}

Le code ci-dessus peut ne pas sembler si facile à comprendre, expliquons-le un par un :

Tout d'abord, déterminez si la commande de tâche soumise est nulle. Si elle est nulle, une exception de pointeur nul sera levée ;

Puis il y a cette phrase, qu'il faut bien comprendre :

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

Comme il s'agit de l'opérateur conditionnel OR, la première moitié de la valeur est calculée en premier. Si le nombre actuel de threads dans le pool de threads n'est pas inférieur à la taille du pool principal, il entrera directement le bloc d'instruction if suivant.

Si le nombre actuel de threads dans le pool de threads est inférieur à la taille du pool principal, exécutez la seconde moitié, c'est-à-dire exécutez

addIfUnderCorePoolSize(command)

Si la méthode addIfUnderCorePoolSize renvoie false après l'exécution de la méthode, continuez à exécuter le bloc d'instruction if suivant, sinon la méthode entière sera exécutée directement.

Si la méthode addIfUnderCorePoolSize renvoie false après l'exécution, jugez :

if (runState == RUNNING && workQueue.offer(command))

Si le pool de threads actuel est à l'état RUNNING, placez la tâche dans la file d'attente du cache des tâches ; si le pool de threads actuel n'est pas à l'état RUNNING ou si la tâche ne parvient pas à être placée dans la file d'attente du cache, exécutez :

addIfUnderMaximumPoolSize(command)

Si l'exécution de la méthode addIfUnderMaximumPoolSize échoue, exécutez la méthode rejet() pour rejeter la tâche.

Retour à l'avant :

if (runState == RUNNING && workQueue.offer(command))

L'exécution de cette phrase, si le pool de threads actuel est dans l'état RUNNING et que la tâche est placée avec succès dans la file d'attente du cache de tâches, continuez à juger :

if (runState != RUNNING || poolSize == 0)

Ce jugement est une mesure d'urgence pour empêcher d'autres threads d'appeler soudainement la méthode shutdown ou shutdownNow pour fermer le pool de threads lorsque la tâche est ajoutée à la file d'attente du cache des tâches. Si c'est le cas, exécutez :

ensureQueuedTaskHandled(command)

Le traitement d'urgence, comme son nom l'indique, consiste à s'assurer que les tâches ajoutées à la file d'attente du cache des tâches sont traitées.

Examinons l'implémentation de deux méthodes clés : addIfUnderCorePoolSize et addIfUnderMaximumPoolSize :

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)
            t = addThread(firstTask);        //创建线程去执行firstTask任务   
        } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

Il s'agit de l'implémentation spécifique de la méthode addIfUnderCorePoolSize.D'après son nom, on peut voir que son intention est d'exécuter la méthode lorsqu'elle est inférieure à la taille du cœur. Regardons son implémentation spécifique. Tout d'abord, le verrou est obtenu, car cet endroit implique le changement de l'état du pool de threads. Tout d'abord, jugez si le nombre de threads dans le pool de threads actuel est inférieur à la taille du pool principal via le if instruction. Certains amis peuvent avoir des doutes : avant l'exécution N'a-t-elle pas été jugée dans la méthode () ? La méthode addIfUnderCorePoolSize ne sera exécutée que si le nombre actuel de threads dans le pool de threads est inférieur à la taille du pool principal. Pourquoi cet endroit continuer à juger? La raison est très simple. Il n'y a pas de verrou dans le processus de jugement précédent. Par conséquent, poolSize peut être inférieur à corePoolSize lorsque la méthode d'exécution est jugée. Après le jugement, d'autres threads soumettent des tâches au pool de threads, ce qui peut provoquer l'erreur poolSize ne doit pas être inférieur à corePoolSize. , vous devez donc continuer à juger à cet endroit. Ensuite, il est jugé si l'état du pool de threads est RUNNING. La raison est très simple, car il est possible que la méthode shutdown ou shutdownNow soit appelée dans d'autres threads. puis exécutez

t = addThread(firstTask);

Cette méthode est également très critique, le paramètre passé est la tâche soumise et la valeur de retour est de type Thread. Jugez ensuite si t est vide ou non. S'il est vide, cela indique que la création du thread a échoué (c'est-à-dire que poolSize>=corePoolSize ou runState n'est pas égal à RUNNING), sinon, la méthode t.start() est appelée pour démarrer le fil.

Examinons l'implémentation de la méthode addThread :

private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务   
    if (t != null) {
        w.thread = t;            //将创建的线程的引用赋值为w的成员变量       
        workers.add(w);
        int nt = ++poolSize;     //当前线程数加1       
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

Dans la méthode addThread, créez d'abord un objet Worker avec la tâche soumise, puis appelez la fabrique de threads threadFactory pour créer un nouveau thread t, puis affectez la référence du thread t à la variable membre thread de l'objet Worker, puis passez les workers. add (w) Ajoute l'objet Worker au jeu de travail.

Regardons l'implémentation de la classe Worker :

private final class Worker implements Runnable {
    private final ReentrantLock runLock = new ReentrantLock();
    private Runnable firstTask;
    volatile long completedTasks;
    Thread thread;
    Worker(Runnable firstTask) {
        this.firstTask = firstTask;
    }
    boolean isActive() {
        return runLock.isLocked();
    }
    void interruptIfIdle() {
        final ReentrantLock runLock = this.runLock;
        if (runLock.tryLock()) {
            try {
        if (thread != Thread.currentThread())
        thread.interrupt();
            } finally {
                runLock.unlock();
            }
        }
    }
    void interruptNow() {
        thread.interrupt();
    }
 
    private void runTask(Runnable task) {
        final ReentrantLock runLock = this.runLock;
        runLock.lock();
        try {
            if (runState < STOP &&
                Thread.interrupted() &&
                runState >= STOP)
            boolean ran = false;
            beforeExecute(thread, task);   //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据
            //自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等           
            try {
                task.run();
                ran = true;
                afterExecute(task, null);
                ++completedTasks;
            } catch (RuntimeException ex) {
                if (!ran)
                    afterExecute(task, ex);
                throw ex;
            }
        } finally {
            runLock.unlock();
        }
    }
 
    public void run() {
        try {
            Runnable task = firstTask;
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);   //当任务队列中没有任务时,进行清理工作       
        }
    }
}

Il implémente en fait l'interface Runnable, donc l'effet du Thread ci-dessus t = threadFactory.newThread(w); est fondamentalement le même que l'effet de la phrase suivante :

Thread t = new Thread(w);

Cela équivaut à transmettre une tâche Runnable et à exécuter ce Runnable dans le thread t.

Puisque Worker implémente l'interface Runnable, la méthode principale naturelle est la méthode run() :

public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        }
    } finally {
        workerDone(this);
    }
}

On peut voir à partir de l'implémentation de la méthode run qu'elle exécute d'abord la tâche firstTask transmise via le constructeur.Après avoir appelé runTask() pour exécuter la firstTask, il continue à récupérer de nouvelles tâches à exécuter via getTask() dans la boucle while Alors où se le procurer ? Naturellement, il est extrait de la file d'attente du cache de tâches. getTask est une méthode de la classe ThreadPoolExecutor, et non une méthode de la classe Worker. Voici l'implémentation de la méthode getTask :

Runnable getTask() {
    for (;;) {
        try {
            int state = runState;
            if (state > SHUTDOWN)
                return null;
            Runnable r;
            if (state == SHUTDOWN)  // Help drain queue
                r = workQueue.poll();
            else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,
                //则通过poll取任务,若等待一定的时间取不到任务,则返回null
                r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
            else
                r = workQueue.take();
            if (r != null)
                return r;
            if (workerCanExit()) {    //如果没取到任务,即r为null,则判断当前的worker是否可以退出
                if (runState >= SHUTDOWN) // Wake up others
                    interruptIdleWorkers();   //中断处于空闲状态的worker
                return null;
            }
            // Else retry
        } catch (InterruptedException ie) {
            // On interruption, re-check runState
        }
    }
}

Dans getTask, jugez d'abord l'état actuel du pool de threads, et si runState est supérieur à SHUTDOWN (c'est-à-dire STOP ou TERMINATED), renvoyez null directement.

Si runState est SHUTDOWN ou RUNNING, la tâche est extraite de la file d'attente du cache de tâches.

Si le nombre de threads dans le pool de threads actuel est supérieur à la taille du pool de base corePoolSize ou si le temps de survie inactif est autorisé à être défini pour les threads dans le pool de base, appelez poll(time, timeUnit) pour récupérer la tâche, cette méthode attendra un certain temps, si la tâche ne peut pas être récupérée, renvoie null.

Ensuite, jugez si la tâche récupérée r est nulle, et si elle est nulle, jugez si le travailleur actuel peut quitter en appelant la méthode workerCanExit(). Jetons un coup d'œil à l'implémentation de workerCanExit() :

private boolean workerCanExit() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    boolean canExit;
    //如果runState大于等于STOP,或者任务缓存队列为空了
    //或者  允许为核心池线程设置空闲存活时间并且线程池中的线程数目大于1
    try {
        canExit = runState >= STOP ||
            workQueue.isEmpty() ||
            (allowCoreThreadTimeOut &&
             poolSize > Math.max(1, corePoolSize));
    } finally {
        mainLock.unlock();
    }
    return canExit;
}

C'est-à-dire que si le pool de threads est à l'état STOP, ou si la file d'attente des tâches est vide, ou si le temps de survie inactif est autorisé à être défini pour le thread du pool principal et que le nombre de threads est supérieur à 1, le travailleur est autorisé à sortir. Si le travailleur est autorisé à quitter, appelez interruptIdleWorkers() pour interrompre le travailleur inactif. Examinons l'implémentation de interruptIdleWorkers() :

void interruptIdleWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)  //实际上调用的是worker的interruptIfIdle()方法
            w.interruptIfIdle();
    } finally {
        mainLock.unlock();
    }
}

Comme on peut le voir à partir de l'implémentation, il appelle en fait la méthode interruptIfIdle() du worker, dans la méthode interruptIfIdle() du worker :

void interruptIfIdle() {
    final ReentrantLock runLock = this.runLock;
    if (runLock.tryLock()) {    //注意这里,是调用tryLock()来获取锁的,因为如果当前worker正在执行任务,锁已经被获取了,是无法获取到锁的
                                //如果成功获取了锁,说明当前worker处于空闲状态
        try {
    if (thread != Thread.currentThread())  
    thread.interrupt();
        } finally {
            runLock.unlock();
        }
    }
}

Il existe ici une méthode de conception très ingénieuse. Si nous concevons le pool de threads, il peut y avoir un thread de répartition des tâches. Lorsqu'un thread s'avère inactif, une tâche sera extraite de la file d'attente du cache de tâches et donnée au thread inactif pour exécution. Cependant, cette méthode n'est pas retenue ici, car elle gérera en plus le thread dispatching de la tâche, ce qui augmentera de manière invisible la difficulté et la complexité. Ici, le thread qui a exécuté la tâche est directement invité à aller dans la file d'attente du cache des tâches pour récupérer le tâche à exécuter. .

Regardons l'implémentation de la méthode addIfUnderMaximumPoolSize. L'idée d'implémentation de cette méthode est très similaire à celle de la méthode addIfUnderCorePoolSize. La seule différence est que la méthode addIfUnderMaximumPoolSize est que le nombre de threads dans le pool de threads atteint le pool principal size et l'ajout de tâches à la file d'attente des tâches échoue. Exécuté en cas de :

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < maximumPoolSize && runState == RUNNING)
            t = addThread(firstTask);
    } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

Voir non, en fait, c'est fondamentalement la même chose que l'implémentation de la méthode addIfUnderCorePoolSize, sauf que poolSize < maximumPoolSize dans la condition de jugement de l'instruction if est différente.

À ce stade, la plupart de mes amis devraient avoir une compréhension de base de l'ensemble du processus depuis le moment où la tâche est soumise au pool de threads jusqu'à l'exécution.

1) Tout d'abord, soyez clair sur la signification de corePoolSize et maximumPoolSize ;

2) Deuxièmement, vous devez savoir pour quel rôle Worker est utilisé ;

3) Pour connaître la stratégie de traitement après la soumission de la tâche au pool de threads, voici quatre points principaux :

  • Si le nombre de threads dans le pool de threads actuel est inférieur à corePoolSize, chaque fois qu'une tâche arrive, un thread sera créé pour exécuter la tâche ;
  • Si le nombre de threads dans le pool de threads actuel est >= corePoolSize, chaque fois qu'une tâche arrive, elle essaiera de l'ajouter à la file d'attente du cache de tâches. Si l'ajout réussit, la tâche attendra qu'un thread inactif la prenne out pour exécution ; si l'ajout échoue (en règle générale, la file d'attente du cache des tâches est pleine), il essaiera de créer un nouveau thread pour exécuter la tâche ;
  • Si le nombre de threads dans le pool de threads actuel atteint le maximumPoolSize, la stratégie de rejet de tâche sera adoptée pour le traitement ;
  • Si le nombre de threads dans le pool de threads est supérieur à corePoolSize, si le temps d'inactivité d'un thread dépasse keepAliveTime, le thread sera terminé jusqu'à ce que le nombre de threads dans le pool de threads ne soit pas supérieur à corePoolSize ; s'il est autorisé à définir le temps de survie des threads dans le pool principal, puis le pool principal Le thread dans le temps d'inactivité dépasse keepAliveTime, le thread sera également terminé.

3. Initialisation des threads dans le pool de threads

Par défaut, une fois le pool de threads créé, il n'y a plus de threads dans le pool de threads et les threads ne sont créés qu'après la soumission des tâches.

En pratique, si vous devez créer un thread immédiatement après la création du pool de threads, vous pouvez le faire de deux manières :

  • prestartCoreThread() : Initialise un thread principal ;
  • prestartAllCoreThreads() : Initialise tous les threads principaux

Voici la mise en œuvre de ces deux méthodes :

public boolean prestartCoreThread() {
    return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}
 
public int prestartAllCoreThreads() {
    int n = 0;
    while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
        ++n;
    return n;
}

Notez que le paramètre transmis ci-dessus est nul. Selon l'analyse de la section 2, si le paramètre transmis est nul, le thread d'exécution final sera bloqué dans la méthode getTask.

r = workQueue.take();

C'est-à-dire attendre une tâche dans la file d'attente des tâches.

4. File d'attente du cache des tâches et stratégie de mise en file d'attente

Nous avons mentionné à plusieurs reprises la file d'attente du cache des tâches, c'est-à-dire workQueue, qui est utilisée pour stocker les tâches en attente d'exécution.

Le type de workQueue est BlockingQueue<Runnable>, qui peut généralement prendre les trois types suivants :

1) ArrayBlockingQueue : une file d'attente premier entré, premier sorti basée sur un tableau, la taille doit être spécifiée lors de la création de la file d'attente ;

2) LinkedBlockingQueue : une file d'attente premier entré, premier sorti basée sur une liste chaînée. Si la taille de la file d'attente n'est pas spécifiée lors de sa création, la valeur par défaut est Integer.MAX_VALUE ;

3) synchronousQueue : Cette file d'attente est spéciale, elle ne sauvegardera pas les tâches soumises, mais créera directement un nouveau thread pour exécuter la nouvelle tâche.

5. Politique de rejet de tâche

Lorsque la file d'attente du cache de tâches du pool de threads est pleine et que le nombre de threads dans le pool de threads atteint le maximumPoolSize, s'il y a encore des tâches à venir, la stratégie de rejet de tâche sera adoptée. Il existe généralement les quatre stratégies suivantes :

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6. Fermeture du pool de threads

ThreadPoolExecutor fournit deux méthodes pour l'arrêt du pool de threads, shutdown() et shutdownNow(), où :

  • shutdown() : le pool de threads ne sera pas terminé immédiatement, mais le sera après que toutes les tâches de la file d'attente du cache de tâches auront été exécutées, mais aucune nouvelle tâche ne sera acceptée.
  • shutdownNow() : terminez immédiatement le pool de threads et essayez d'interrompre la tâche en cours d'exécution, puis effacez la file d'attente du cache de tâches et renvoyez la tâche qui n'a pas été exécutée.

7. Ajustement dynamique de la capacité du pool de threads

ThreadPoolExecutor fournit des méthodes pour ajuster dynamiquement la taille du pool de threads : setCorePoolSize() et setMaximumPoolSize(),

  • setCorePoolSize : définit la taille du pool de cœurs
  • setMaximumPoolSize : définit le nombre maximal de threads que le pool de threads peut créer

Lorsque les paramètres ci-dessus passent de petit à grand, ThreadPoolExecutor effectue une affectation de thread et peut immédiatement créer de nouveaux threads pour exécuter des tâches.

3. Exemple d'utilisation

Nous avons évoqué précédemment le principe d'implémentation du pool de threads, dans cette section, regardons son utilisation spécifique :

public class Test {
     public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<15;i++){
             MyTask myTask = new MyTask(i);
             executor.execute(myTask);
             System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
             executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
}
 
 
class MyTask implements Runnable {
    private int taskNum;
     
    public MyTask(int num) {
        this.taskNum = num;
    }
     
    @Override
    public void run() {
        System.out.println("正在执行task "+taskNum);
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"执行完毕");
    }
}

Résultats du :

正在执行task 0
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 1
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 2
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 3
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 4
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 10
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 11
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 12
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 13
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 14
task 3执行完毕
task 0执行完毕
task 2执行完毕
task 1执行完毕
正在执行task 8
正在执行task 7
正在执行task 6
正在执行task 5
task 4执行完毕
task 10执行完毕
task 11执行完毕
task 13执行完毕
task 12执行完毕
正在执行task 9
task 14执行完毕
task 8执行完毕
task 5执行完毕
task 7执行完毕
task 6执行完毕
task 9执行完毕

Il ressort des résultats d'exécution que lorsque le nombre de threads dans le pool de threads est supérieur à 5, la tâche est placée dans la file d'attente du cache des tâches et lorsque la file d'attente du cache des tâches est pleine, un nouveau thread est créé. Si dans le programme ci-dessus, la boucle for est modifiée pour exécuter 20 tâches, une exception de rejet de tâche sera levée.

Cependant, dans la doc java, nous ne recommandons pas d'utiliser directement ThreadPoolExecutor, mais d'utiliser plusieurs méthodes statiques fournies dans la classe Executors pour créer un pool de threads :

Executors.newCachedThreadPool();        //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池
Executors.newFixedThreadPool(int);    //创建固定容量大小的缓冲池

Voici l'implémentation spécifique de ces trois méthodes statiques :

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

De leur implémentation spécifique, ils appellent réellement ThreadPoolExecutor, mais les paramètres ont été configurés.

Les valeurs corePoolSize et maximumPoolSize du pool de threads créées par newFixedThreadPool sont égales, et il utilise LinkedBlockingQueue ;

newSingleThreadExecutor définit à la fois corePoolSize et maximumPoolSize sur 1, et utilise également LinkedBlockingQueue ;

newCachedThreadPool définit corePoolSize sur 0, maximumPoolSize sur Integer.MAX_VALUE et utilise SynchronousQueue, ce qui signifie que lorsqu'une tâche arrive, un thread est créé pour s'exécuter, et lorsque le thread est inactif pendant plus de 60 secondes, le thread est détruit.

En pratique, si les trois méthodes statiques fournies par Executors peuvent répondre aux exigences, essayez d'utiliser les trois méthodes fournies par celui-ci, car il est un peu gênant de configurer manuellement les paramètres de ThreadPoolExecutor par vous-même, et il doit être configuré selon le type et le nombre de tâches réelles.

De plus, si ThreadPoolExecutor ne répond pas aux exigences, vous pouvez hériter de la classe ThreadPoolExecutor et la réécrire.

Quatrièmement, comment configurer raisonnablement la taille du pool de threads

Cette section traite d'un sujet plus important : comment configurer raisonnablement la taille du pool de threads, à titre de référence uniquement.

Généralement, la taille du pool de threads doit être configurée en fonction du type de tâche :

S'il s'agit d'une tâche gourmande en CPU, vous devez presser le CPU autant que possible. La valeur de référence peut être définie sur  N CPU+1

S'il s'agit d'une tâche gourmande en E/S, la valeur de référence peut être définie sur 2* N CPU

Bien sûr, il ne s'agit que d'une valeur de référence et les paramètres spécifiques doivent être ajustés en fonction de la situation réelle. Par exemple, vous pouvez d'abord définir la taille du pool de threads sur la valeur de référence, puis observer le fonctionnement de la tâche, la charge du système, et l'utilisation des ressources pour effectuer les ajustements appropriés.

Je suppose que tu aimes

Origine blog.csdn.net/qq_41701956/article/details/123488240
conseillé
Classement