Comprendre l'article et comprendre le pool de threads Java - Explication détaillée de ThreadPoolExecutor

d'abord avec des questions pour comprendre

  • Pourquoi y a-t-il un pool de threads ?
  • Quelles sont les manières dont Java implémente et gère le pool de threads ? Donnez un exemple simple d'utilisation.
  • Pourquoi de nombreuses entreprises ne sont-elles pas autorisées à utiliser des exécuteurs pour créer des pools de threads ? Alors, comment recommandez-vous de les utiliser ?
  • Quels sont les paramètres de configuration de base de ThreadPoolExecutor ? Veuillez expliquer brièvement
  • Quels sont les trois pools de threads que ThreadPoolExecutor peut créer ?
  • Que se passe-t-il lorsque la file d'attente est pleine et que le nombre de nœuds de calcul atteint maxSize ?
  • Dites-moi quelles sont les stratégies RejectedExecutionHandler de ThreadPoolExecutor ? Quelle est la stratégie par défaut ?
  • Parlez brièvement du mécanisme d'exécution des tâches du pool de threads ? execute -> addWorker -> runworker (getTask)
  • Comment les tâches sont-elles soumises dans le pool de threads ?
  • Comment les tâches du pool de threads sont-elles fermées ?
  • Quels facteurs de configuration doivent être pris en compte lors de la configuration du pool de threads ?
  • Comment surveiller l'état du pool de threads ?

 Pourquoi avoir un pool de threads

Le pool de threads peut allouer, régler et surveiller les threads de manière uniforme :

  • Réduire la consommation de ressources (les threads sont créés indéfiniment puis détruits après utilisation)
  • Améliorer la vitesse de réponse (pas besoin de créer des threads)
  • Améliorer la gérabilité des threads

# Exemple de ThreadPoolExecutor

Comment Java implémente-t-il et gère-t-il le pool de threads ?

À partir de JDK 5, l'unité de travail est séparée du mécanisme d'exécution. L'unité de travail comprend Runnable et Callable, et le mécanisme d'exécution est fourni par le framework Executor.

  • WorkerThread
public class WorkerThread implements Runnable {
     
    private String command;
     
    public WorkerThread(String s){
        this.command=s;
    }
 
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
        processCommand();
        System.out.println(Thread.currentThread().getName()+" End.");
    }
 
    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public String toString(){
        return this.command;
    }
}
  • SimpleThreadPool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class SimpleThreadPool {
 
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread("" + i);
            executor.execute(worker);
          }
        executor.shutdown(); // This will make the executor accept no new threads and finish all existing threads in the queue
        while (!executor.isTerminated()) { // Wait until all threads are finish,and also you can use "executor.awaitTermination();" to wait
        }
        System.out.println("Finished all threads");
    }

}

Dans le programme, nous avons créé un pool de threads avec une taille fixe de cinq threads de travail. Attribuez ensuite dix travaux au pool de threads. Étant donné que la taille du pool de threads est de cinq, cinq threads de travail seront lancés pour traiter cinq travaux en premier, et les autres travaux seront en attente. Une fois qu'un travail est terminé, le thread de travail récupérez-le lorsqu'il est inactif. Attendez que d'autres travaux de la file d'attente s'exécutent.

Voici la sortie du programme ci-dessus.

pool-1-thread-2 Start. Command = 1
pool-1-thread-4 Start. Command = 3
pool-1-thread-1 Start. Command = 0
pool-1-thread-3 Start. Command = 2
pool-1-thread-5 Start. Command = 4
pool-1-thread-4 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
pool-1-thread-3 End.
pool-1-thread-3 Start. Command = 8
pool-1-thread-2 End.
pool-1-thread-2 Start. Command = 9
pool-1-thread-1 Start. Command = 7
pool-1-thread-5 Start. Command = 6
pool-1-thread-4 Start. Command = 5
pool-1-thread-2 End.
pool-1-thread-4 End.
pool-1-thread-3 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
Finished all threads

La sortie montre qu'il n'y a que cinq threads nommés "pool-1-thread-1" à "pool-1-thread-5" dans le pool de threads du début à la fin, et ces cinq threads ne meurent pas avec l'achèvement du travail, il existera toujours et est responsable de l'exécution des tâches assignées au pool de threads jusqu'à ce que le pool de threads meure.

La classe Executors fournit une implémentation simple d'ExecutorService qui utilise ThreadPoolExecutor, mais ThreadPoolExecutor fournit bien plus que cela. Nous pouvons spécifier le nombre de threads actifs lors de la création d'une instance ThreadPoolExecutor, nous pouvons également limiter la taille du pool de threads et créer notre propre implémentation de RejectedExecutionHandler pour gérer le travail qui ne peut pas tenir dans la file d'attente de travail.

Voici l'implémentation de notre interface RejectedExecutionHandler personnalisée.

  • RejectedExecutionHandlerImpl.java
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
 
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
 
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(r.toString() + " is rejected");
    }
 
}

ThreadPoolExecutor fournit des méthodes que nous pouvons utiliser pour interroger l'état actuel de l'exécuteur, la taille du pool de threads, le nombre de threads actifs et le nombre de tâches. J'utilise donc un thread de surveillance pour imprimer des informations sur l'exécuteur à des intervalles de temps spécifiques.

  • MyMonitorThread.java
import java.util.concurrent.ThreadPoolExecutor;
 
public class MyMonitorThread implements Runnable
{
    private ThreadPoolExecutor executor;
     
    private int seconds;
     
    private boolean run=true;
 
    public MyMonitorThread(ThreadPoolExecutor executor, int delay)
    {
        this.executor = executor;
        this.seconds=delay;
    }
     
    public void shutdown(){
        this.run=false;
    }
 
    @Override
    public void run()
    {
        while(run){
                System.out.println(
                    String.format("[monitor] [%d/%d] Active: %d, Completed: %d, Task: %d, isShutdown: %s, isTerminated: %s",
                        this.executor.getPoolSize(),
                        this.executor.getCorePoolSize(),
                        this.executor.getActiveCount(),
                        this.executor.getCompletedTaskCount(),
                        this.executor.getTaskCount(),
                        this.executor.isShutdown(),
                        this.executor.isTerminated()));
                try {
                    Thread.sleep(seconds*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
             
    }
}

Voici un exemple d'implémentation de pool de threads à l'aide de ThreadPoolExecutor.

  • WorkerPool.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class WorkerPool {
 
    public static void main(String args[]) throws InterruptedException{
        //RejectedExecutionHandler implementation
        RejectedExecutionHandlerImpl rejectionHandler = new RejectedExecutionHandlerImpl();
        //Get the ThreadFactory implementation to use
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        //creating the ThreadPoolExecutor
        ThreadPoolExecutor executorPool = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2), threadFactory, rejectionHandler);
        //start the monitoring thread
        MyMonitorThread monitor = new MyMonitorThread(executorPool, 3);
        Thread monitorThread = new Thread(monitor);
        monitorThread.start();
        //submit work to the thread pool
        for(int i=0; i<10; i++){
            executorPool.execute(new WorkerThread("cmd"+i));
        }
         
        Thread.sleep(30000);
        //shut down the pool
        executorPool.shutdown();
        //shut down the monitor thread
        Thread.sleep(5000);
        monitor.shutdown();
         
    }
}

Notez que lors de l'initialisation de ThreadPoolExecutor, nous gardons la taille initiale du pool à 2, la taille maximale du pool à 4 et la taille de la file d'attente de travail à 2. Ainsi, s'il y a déjà quatre tâches en cours d'exécution et que davantage de tâches sont affectées à ce moment, la file d'attente de travail n'en contiendra que deux (nouvelles tâches) et les autres seront traitées par RejectedExecutionHandlerImpl.

La sortie du programme ci-dessus peut confirmer le point ci-dessus.

pool-1-thread-1 Start. Command = cmd0
pool-1-thread-4 Start. Command = cmd5
cmd6 is rejected
pool-1-thread-3 Start. Command = cmd4
pool-1-thread-2 Start. Command = cmd1
cmd7 is rejected
cmd8 is rejected
cmd9 is rejected
[monitor] [0/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-4 End.
pool-1-thread-1 End.
pool-1-thread-2 End.
pool-1-thread-3 End.
pool-1-thread-1 Start. Command = cmd3
pool-1-thread-4 Start. Command = cmd2
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-1 End.
pool-1-thread-4 End.
[monitor] [4/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true

Notez le changement dans le nombre de tâches actives des exécuteurs, de tâches terminées et de toutes les tâches terminées. Nous pouvons appeler la méthode shutdown () pour terminer toutes les tâches soumises et terminer le pool de threads.

 Utilisation détaillée de ThreadPoolExecutor

En fait, le principe d'implémentation du pool de threads java est très simple, pour le dire crûment, il s'agit d'une collection de threads workerSet et d'une file bloquante workQueue. Lorsque l'utilisateur soumet une tâche (c'est-à-dire un thread) au pool de threads, le pool de threads place d'abord la tâche dans la workQueue. Les threads de workerSet obtiendront en permanence des threads de workQueue et les exécuteront. Lorsqu'il n'y a pas de tâche dans la file d'attente, le travailleur bloquera jusqu'à ce qu'il y ait une tâche dans la file d'attente, puis la retirera et poursuivra l'exécution.

 Exécuter le principe

Une fois qu'une tâche est soumise au pool de threads :

  1. Tout d'abord, si le nombre de threads en cours d'exécution dans le pool de threads est inférieur à corePoolSize. Si oui, créez un nouveau thread de travail pour effectuer la tâche. Si toutes les tâches sont en cours d'exécution, passez à 2.
  2. Déterminez si la BlockingQueue est pleine, et si ce n'est pas le cas, placez le thread dans la BlockingQueue. Sinon passez au 3.
  3. Si la création d'un nouveau thread de travail entraîne le dépassement du maximumPoolSize par le nombre de threads en cours d'exécution, il est transmis au RejectedExecutionHandler pour gérer la tâche.

Lorsque ThreadPoolExecutor crée un nouveau thread, mettez à jour le statut ctl du pool de threads via CAS.

#paramètres _

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)
  • corePoolSizeNombre de threads principaux dans le pool de threads. Lorsqu'une tâche est soumise, le pool de threads crée un nouveau thread pour exécuter la tâche jusqu'à ce que le nombre actuel de threads soit égal à corePoolSize. Même s'il existe d'autres threads inactifs qui peuvent exécuter de nouvelles tâches , les threads continueront d'être créés ; si le nombre actuel de threads est corePoolSize et que les tâches qui continuent d'être soumises sont enregistrées dans la file d'attente de blocage et attendent d'être exécutées ; si la méthode prestartAllCoreThreads() du pool de threads est exécutée , le pool de threads créera et démarrera tous les threads principaux à l'avance.

  • workQueueUne file d'attente de blocage utilisée pour contenir les tâches en attente d'exécution.

    • ArrayBlockingQueue: Une file d'attente de blocage bornée basée sur une structure de tableau, triant les tâches par FIFO ;
    • LinkedBlockingQueue: Blocage de la file d'attente basée sur la structure de la liste chaînée, tri des tâches par FIFO, le débit est généralement supérieur à ArrayBlockingQueue ;
    • SynchronousQueue: Une file d'attente bloquante qui ne stocke pas d'éléments, chaque opération d'insertion doit attendre qu'un autre thread appelle l'opération de suppression, sinon l'opération d'insertion est toujours bloquée et le débit est généralement supérieur à LinkedBlockingQueue ;
    • PriorityBlockingQueue: File d'attente de blocage illimitée avec priorité ;

LinkedBlockingQueueArrayBlockingQueueC'est mieux que les performances d'insertion et de suppression de nœuds, mais les deux doivent être verrouillés lors de l'exécution de tâches. put()À l'aide d'un algorithme sans verrouillage, l'exécution est jugée en fonction de l'état du nœud sans utiliser de verrous. Le noyau est .take()SynchronousQueueTransfer.transfer()

  • maximumPoolSize Le nombre maximal de threads autorisés dans le pool de threads. Si la file d'attente de blocage actuelle est pleine et que la tâche continue d'être soumise, un nouveau thread sera créé pour exécuter la tâche, à condition que le nombre actuel de threads soit inférieur au maximumPoolSize ; lorsque la file d'attente de blocage est une file d'attente illimitée, le maximumPoolSize ne fonctionnera pas car il ne peut pas être soumis au pool de threads principal. Le thread sera continuellement placé dans la workQueue.

  • keepAliveTime Le temps de survie lorsque le thread est inactif, c'est-à-dire que lorsque le thread n'a aucune tâche à exécuter, le thread continue à survivre ; par défaut, ce paramètre n'est utile que lorsque le nombre de threads est supérieur à corePoolSize et que les threads inactifs dépassent ce le temps sera terminé;

  • unit Unité de keepAliveTime

  • threadFactory Créer une fabrique de threads. Grâce à une fabrique de threads personnalisée, vous pouvez définir un nom de thread identifiable pour chaque thread nouvellement créé. La valeur par défaut est DefaultThreadFactory

  • handler La stratégie de saturation du pool de threads. Lorsque la file d'attente de blocage est pleine et qu'il n'y a pas de threads de travail inactifs, si vous continuez à soumettre des tâches, vous devez adopter une stratégie pour traiter la tâche. Le pool de threads propose 4 stratégies :

    • AbortPolicy: Lancer une exception directement, la stratégie par défaut ;
    • CallerRunsPolicy: Utilisez le thread où se trouve l'appelant pour exécuter la tâche ;
    • DiscardOldestPolicy : Supprimer la tâche supérieure dans la file d'attente de blocage et exécuter la tâche en cours ;
    • DiscardPolicy: annuler la tâche directement ;

Bien sûr, vous pouvez également implémenter l'interface RejectedExecutionHandler en fonction du scénario de l'application et personnaliser la stratégie de saturation, comme la journalisation ou le stockage persistant des tâches qui ne peuvent pas être traitées.

trois sortes

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}

Une fois que le nombre de threads dans le pool de threads atteint corePoolSize, même si le pool de threads n'a pas de tâches exécutables, les threads ne seront pas libérés.

La file d'attente de travail de FixedThreadPool est une file d'attente illimitée LinkedBlockingQueue (la capacité de la file d'attente est Integer.MAX_VALUE), ce qui entraînera les problèmes suivants :

  • Le nombre de threads dans le pool de threads ne dépasse pas corePoolSize, ce qui conduit à maximumPoolSize et keepAliveTime seront des paramètres inutiles
  • En raison de l'utilisation de files d'attente illimitées, FixedThreadPool ne refusera jamais, c'est-à-dire que la stratégie de saturation échouera

 newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

Il n'y a qu'un seul thread dans le pool de threads initialisé. Si le thread se termine anormalement, un nouveau thread sera recréé pour continuer l'exécution de la tâche. Le seul thread peut garantir l'ordre d'exécution des tâches soumises.

En raison de l'utilisation de files d'attente illimitées, SingleThreadPool ne refusera jamais, c'est-à-dire que la stratégie de saturation échouera

 newCachedThreadPool

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

Le nombre de threads dans le pool de threads peut atteindre Integer.MAX_VALUE, qui est 2147483647, et SynchronousQueue est utilisé en interne comme file d'attente de blocage ; contrairement au pool de threads créé par newFixedThreadPool, newCachedThreadPool libère automatiquement les ressources de thread lorsqu'il n'y a pas de tâche à exécuter et le temps d'inactivité du thread dépasse keepAliveTime. Lors de la soumission d'une nouvelle tâche, s'il n'y a pas de thread inactif, la création d'un nouveau thread pour exécuter la tâche entraînera une certaine surcharge du système ; le processus d'exécution est légèrement différent des deux précédents :

  • Le thread principal appelle la méthode offer() de SynchronousQueue et la place dans la tâche. S'il y a un thread inactif dans le pool de threads essayant de lire la tâche de SynchronousQueue à ce moment, c'est-à-dire poll() de SynchronousQueue est appelée, le thread principal transmet la tâche au thread inactif. Sinon, faites (2)
  • Lorsque le pool de threads est vide ou qu'il n'y a pas de threads inactifs, créez un nouveau thread pour exécuter la tâche.
  • Si le thread qui a terminé l'exécution de la tâche est toujours inactif dans les 60 secondes, il sera terminé. Par conséquent, le CachedThreadPool qui a été inactif pendant une longue période ne contiendra aucune ressource de thread.

Fermer le pool de threads

Parcourez tous les threads du pool de threads et appelez la méthode d'interruption du thread un par un pour interrompre le thread.

Méthode d'arrêt - arrêt

Définissez l'état du thread dans le pool de threads sur l'état SHUTDOWN, puis interrompez tous les threads qui n'exécutent pas de tâches.

Méthode d'arrêt - shutdownNow

Définissez l'état du thread dans le pool de threads sur l'état STOP, puis arrêtez tous les threads qui exécutent ou suspendent des tâches. Appelez simplement l'une de ces deux méthodes d'arrêt, isShutDown() renvoie true. Lorsque toutes les tâches sont fermées avec succès, isTerminated() renvoie vrai.

Code source détaillé de ThreadPoolExecutor

quelques propriétés clés

//这个属性是用来存放 当前运行的worker数量以及线程池状态的
//int是32位的,这里把int的高3位拿来充当线程池状态的标志位,后29位拿来充当当前运行worker的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
//worker的集合,用set来存放
private final HashSet<Worker> workers = new HashSet<Worker>();
//历史达到的worker数最大值
private int largestPoolSize;
//当队列满了并且worker的数量达到maxSize的时候,执行具体的拒绝策略
private volatile RejectedExecutionHandler handler;
//超出coreSize的worker的生存时间
private volatile long keepAliveTime;
//常驻worker的数量
private volatile int corePoolSize;
//最大worker的数量,一般当workQueue满了才会用到这个参数
private volatile int maximumPoolSize;

état interne

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

Parmi eux, la variable AtomicInteger ctl est très puissante : utilisez les 29 bits inférieurs pour indiquer le nombre de threads dans le pool de threads, et utilisez les 3 bits supérieurs pour indiquer l'état d'exécution du pool de threads :

  • RUNNING : -1 << COUNT_BITS, c'est-à-dire que les 3 bits supérieurs sont 111. Le pool de threads dans cet état recevra de nouvelles tâches et traitera les tâches dans la file d'attente de blocage ;
  • SHUTDOWN : 0 << COUNT_BITS, c'est-à-dire que les 3 bits supérieurs sont 000, le pool de threads dans cet état ne recevra pas de nouvelles tâches, mais traitera les tâches dans la file d'attente de blocage ;
  • STOP : 1 << COUNT_BITS, c'est-à-dire que les 3 bits supérieurs sont 001, les threads dans cet état ne recevront pas de nouvelles tâches, ne traiteront pas non plus les tâches dans la file d'attente de blocage et interrompront les tâches en cours ;
  • RANGEMENT : 2 << COUNT_BITS, c'est-à-dire que les 3 bits supérieurs sont 010, toutes les tâches ont été terminées ;
  • TERMINATED: 3 << COUNT_BITS, c'est-à-dire que les 3 bits supérieurs sont 011, la méthode terminated() a été exécutée

# Exécution des tâches

exécuter -> addWorker -> runworker (getTask)

Le thread de travail du pool de threads est implémenté par la classe Woker. Sous la garantie du verrou ReentrantLock, l'instance de Woker est insérée dans le HashSet et le thread dans le Woker est démarré. À partir de l'implémentation de la méthode de construction de la classe Woker, on peut constater que lorsque la fabrique de threads crée un thread de thread, elle le passe dans l'instance de Woker elle-même en tant que paramètre.Lorsque la méthode start est exécutée pour démarrer le thread de thread, l'essentiel est d'exécuter la méthode runWorker du worker. Une fois l'exécution de firstTask terminée, la tâche en attente est obtenue à partir de la file d'attente de blocage via la méthode getTask. S'il n'y a pas de tâche dans la file d'attente, la méthode getTask sera bloquée et suspendue et n'occupera pas les ressources du processeur ;

# méthode execute()

ThreadPoolExecutor.execute(task)实现了Executor.execute(task)

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {  
    //workerCountOf获取线程池的当前线程数;小于corePoolSize,执行addWorker创建新线程执行command任务
       if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // double check: c, recheck
    // 线程池处于RUNNING状态,把提交的任务成功放入阻塞队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // recheck and if necessary 回滚到入队操作前,即倘若线程池shutdown状态,就remove(command)
        //如果线程池没有RUNNING,成功从阻塞队列中删除任务,执行reject方法处理任务
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //线程池处于running状态,但是没有线程,则创建线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 往线程池中创建新的线程失败,则reject任务
    else if (!addWorker(command, false))
        reject(command);
}
  • Pourquoi avez-vous besoin de vérifier l'état du pool de threads ?

Dans un environnement multithread, l'état du pool de threads change tout le temps et ctl.get() est une opération non atomique. Il est très probable que l'état du pool de threads change juste après l'état de le pool de threads est obtenu. Juger s'il faut ajouter la commande au workque est l'état avant le pool de threads. S'il n'y a pas de double vérification, si le pool de threads est dans un état non en cours d'exécution (ce qui est susceptible de se produire dans un environnement multithread), la commande ne sera jamais exécutée.

# méthode addWorker

À partir de l'implémentation de la méthode execute, nous pouvons voir que addWorker est principalement responsable de la création de nouveaux threads et de l'exécution de tâches. Lorsque le pool de threads crée de nouveaux threads pour effectuer des tâches, il doit acquérir un verrou global :

private final ReentrantLock mainLock = new ReentrantLock();
private boolean addWorker(Runnable firstTask, boolean core) {
    // CAS更新线程池数量
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 线程池重入锁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();  // 线程启动,执行任务(Worker.thread(firstTask).start());
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

# La méthode runworker de la classe Worker

 private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
     Worker(Runnable firstTask) {
         setState(-1); // inhibit interrupts until runWorker
         this.firstTask = firstTask;
         this.thread = getThreadFactory().newThread(this); // 创建线程
     }
     /** Delegates main run loop to outer runWorker  */
     public void run() {
         runWorker(this);
     }
     // ...
 }
  • Héritant de la classe AQS, il est commode de réaliser l'opération de suspension du thread de travail ;
  • Implémentez l'interface Runnable, qui peut s'exécuter en tant que tâche dans le thread de travail ;
  • La tâche firstTask actuellement soumise est transmise en tant que paramètre au constructeur de Worker ;

Certaines propriétés ont également des constructeurs :

//运行的线程,前面addWorker方法中就是直接通过启动这个线程来启动这个worker
final Thread thread;
//当一个worker刚创建的时候,就先尝试执行这个任务
Runnable firstTask;
//记录完成任务的数量
volatile long completedTasks;

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    //创建一个Thread,将自己设置给他,后面这个thread启动的时候,也就是执行worker的run方法
    this.thread = getThreadFactory().newThread(this);
}   

La méthode runWorker est le cœur du pool de threads :

  • Une fois le thread démarré, le verrou est libéré via la méthode de déverrouillage et l'état d'AQS est défini sur 0, indiquant que l'opération peut être interrompue.
  • Worker exécute firstTask ou obtient des tâches de workQueue :
    • Effectuer des opérations de verrouillage pour s'assurer que les threads ne sont pas interrompus par d'autres threads (sauf si le pool de threads est interrompu)
    • Vérifiez l'état du pool de threads, si le pool de threads est interrompu, le thread en cours sera interrompu.
    • exécuter avant d'exécuter
    • La méthode run pour exécuter la tâche
    • Exécuter la méthode afterExecute
    • Déverrouiller l'opération

Récupérez la tâche en attente de la file d'attente de blocage via la méthode getTask. S'il n'y a pas de tâche dans la file d'attente, la méthode getTask sera bloquée et suspendue et n'occupera pas les ressources du processeur ;

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 先执行firstTask,再从workerQueue中取task(getTask())

        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

# méthode getTask

Examinons la méthode getTask(), qui implique l'utilisation de keepAliveTime. À partir de cette méthode, nous pouvons voir comment le pool de threads détruit la partie du travailleur qui dépasse le corePoolSize.

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

Notez que ce morceau de code est la clé du fonctionnement de keepAliveTime :

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();

allowCoreThreadTimeOut est faux, le thread ne sera pas détruit même s'il est inactif ; s'il est vrai, il sera détruit s'il est toujours inactif pendant keepAliveTime.

Si le thread est autorisé à attendre inactif sans être détruit timed == false, workQueue.take task : Si la file d'attente de blocage est vide, le thread en cours sera suspendu et attendra ; lorsqu'une tâche est ajoutée dans la file d'attente, le thread sera réveillé, et la méthode take renvoie la tâche, et s'exécute ;

Si le thread n'autorise pas l'inactivité sans fin temporisée == true, tâche workQueue.poll : si la file d'attente de blocage n'a toujours aucune tâche pendant la durée keepAliveTime, renvoie null ;

 remise des tâches

  1. soumettez la tâche, attendez que le pool de threads s'exécute
  2. Lorsque la méthode get de la classe FutureTask est exécutée, le thread principal sera encapsulé dans un nœud WaitNode et stocké dans la liste des serveurs, et sera bloqué pour attendre le résultat en cours d'exécution ;
  3. Une fois l'exécution de la tâche FutureTask terminée, définissez le waitNode correspondant aux serveurs sur null via UNSAFE, et réveillez le thread principal via la méthode unpark de la classe LockSupport ;
public class Test{
    public static void main(String[] args) {

        ExecutorService es = Executors.newCachedThreadPool();
        Future<String> future = es.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "future result";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Dans les scénarios commerciaux réels, Future et Callable apparaissent essentiellement par paires, Callable est responsable de la génération des résultats et Future est responsable de l'obtention des résultats.

  1. L'interface Callable est similaire à Runnable, sauf que Runnable ne renvoie pas de valeur.
  2. En plus de renvoyer les résultats normaux de la tâche Callable, si une exception se produit, l'exception sera également renvoyée, c'est-à-dire que Future peut obtenir divers résultats de la tâche d'exécution asynchrone ;
  3. La méthode Future.get entraînera le blocage du thread principal jusqu'à ce que la tâche Callable soit exécutée ;

# soumettre la méthode

AbstractExecutorService.submit() implémente ExecutorService.submit() pour obtenir la valeur de retour après l'exécution, et ThreadPoolExecutor est une sous-classe de AbstractExecutorService.submit(), donc la méthode submit est également une méthode de ThreadPoolExecutor`.

// submit()在ExecutorService中的定义
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);
// submit方法在AbstractExecutorService中的实现
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    // 通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

La tâche Callable soumise via la méthode submit sera encapsulée dans un objet FutureTask. Soumettez la FutureTask au pool de threads via la méthode Executor.execute pour attendre l'exécution, et l'exécution finale est la méthode d'exécution de la FutureTask ;

Objet FutureTaskFutureTask object

public class FutureTask<V> implements RunnableFuture<V>FutureTask peut être soumis au pool de threads pour être exécuté (exécuté par la méthode run de FutureTask)

  • état interne
/* The run state of this task, initially NEW. 
    * ...
    * Possible state transitions:
    * NEW -> COMPLETING -> NORMAL
    * NEW -> COMPLETING -> EXCEPTIONAL
    * NEW -> CANCELLED
    * NEW -> INTERRUPTING -> INTERRUPTED
    */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

La modification de l'état interne est modifiée par sun.misc.Unsafe

  • obtenir la méthode
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
} 

Le thread principal est bloqué en interne via la méthode awaitDone, et l'implémentation spécifique est la suivante :

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}
  1. Si le thread principal est interrompu, une exception d'interruption est levée ;
  2. Déterminez l'état actuel de la FutureTask, s'il est supérieur à COMPLETING, cela signifie que la tâche a été exécutée, et revenez directement ;
  3. Si l'état actuel est égal à COMPLETING, cela signifie que la tâche a été exécutée. À ce stade, le thread principal n'a besoin que de céder des ressources cpu via la méthode yield et d'attendre que l'état devienne NORMAL ;
  4. Encapsulez le thread actuel via la classe WaitNode et ajoutez-le à la liste des serveurs via UNSAFE ;
  5. Enfin, le fil est suspendu à travers le parc ou parkNanos de LockSupport ;

méthode d'exécution

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

La méthode FutureTask.run est exécutée dans le pool de threads, pas dans le thread principal

  1. En exécutant la méthode call de la tâche Callable ;
  2. Si l'appel est exécuté avec succès, le résultat est enregistré via la méthode set ;
  3. S'il y a une exception dans l'exécution de l'appel, enregistrez l'exception via setException ;

clôture des tâches

La méthode shutdown définira l'état du pool de threads sur SHUTDOWN. Une fois que le pool de threads entrera dans cet état, il refusera d'accepter des tâches, puis toutes les tâches restantes seront exécutées.

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //检查是否可以关闭线程
        checkShutdownAccess();
        //设置线程池状态
        advanceRunState(SHUTDOWN);
        //尝试中断worker
        interruptIdleWorkers();
            //预留方法,留给子类实现
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //遍历所有的worker
        for (Worker w : workers) {
            Thread t = w.thread;
            //先尝试调用w.tryLock(),如果获取到锁,就说明worker是空闲的,就可以直接中断它
            //注意的是,worker自己本身实现了AQS同步框架,然后实现的类似锁的功能
            //它实现的锁是不可重入的,所以如果worker在执行任务的时候,会先进行加锁,这里tryLock()就会返回false
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

ShutdownNow fait un travail relativement bon. Il définit d'abord l'état du pool de threads sur STOP, puis rejette toutes les tâches soumises. Enfin, interrompez les travailleurs en cours d'exécution à gauche et à droite, puis effacez la file d'attente des tâches.

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        //检测权限
        advanceRunState(STOP);
        //中断所有的worker
        interruptWorkers();
        //清空任务队列
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //遍历所有worker,然后调用中断方法
        for (Worker w : workers)
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

 compréhension plus profonde

Pourquoi le pool de threads n'est-il pas autorisé à utiliser des exécuteurs pour créer ? Quelle est la méthode recommandée ?

Les pools de threads ne peuvent pas être créés à l'aide d'exécuteurs, mais via ThreadPoolExecutor. Cette méthode de traitement permet aux étudiants qui écrivent d'être plus clairs sur les règles de fonctionnement du pool de threads et d'éviter le risque d'épuisement des ressources. Description : Les inconvénients de chaque méthode d'exécuteurs :

  • newFixedThreadPool et newSingleThreadExecutor : le principal problème est que la file d'attente de traitement des requêtes accumulées peut consommer une très grande quantité de mémoire, voire un MOO.
  • newCachedThreadPool et newScheduledThreadPool : le problème principal est que le nombre maximal de threads est Integer.MAX_VALUE, ce qui peut créer un très grand nombre de threads, voire OOM.

 Méthode recommandée 1

Introduit pour la première fois : package commons-lang3

ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

#méthode recommandée 2

Introduit pour la première fois : package com.google.guava

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

// excute
pool.execute(()-> System.out.println(Thread.currentThread().getName()));

 //gracefully shutdown
pool.shutdown();

#méthode recommandée 3

Méthode de pool de threads de configuration Spring : le bean d'usine de threads personnalisé doit implémenter ThreadFactory, vous pouvez vous référer à d'autres classes d'implémentation par défaut de cette interface et l'utiliser pour injecter directement le bean et appeler la méthode execute (Runnable task)

    <bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />

    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    
    //in code
    userThreadPool.execute(thread);

 Facteurs à prendre en compte lors de la configuration du pool de threads

Du point de vue des quatre points de vue de la priorité des tâches, du temps d'exécution des tâches, de la nature des tâches (intensif pour le processeur/intensif pour les E/S) et des dépendances des tâches. Et utilisez des files d'attente de travail délimitées aussi proches que possible.

Des tâches de nature différente peuvent être traitées séparément à l'aide de pools de threads de différentes tailles :

  • Processeur intensif : aussi peu de threads que possible, Ncpu+1
  • I/O-intensive : autant de threads que possible, Ncpu*2, tels que le pool de connexion à la base de données
  • Hybride : les tâches gourmandes en CPU et les tâches gourmandes en E/S ont peu de différence dans le temps d'exécution et sont divisées en deux pools de threads ; sinon, il n'est pas nécessaire de les diviser.

 Surveiller l'état du pool de threads

Les méthodes suivantes de ThreadPoolExecutor peuvent être utilisées :

  • getTaskCount()Renvoie le nombre total approximatif de tâches dont l'exécution a déjà été planifiée.
  • getCompletedTaskCount()Renvoie le nombre total approximatif de tâches dont l'exécution est terminée. Renvoie moins de résultats que getTaskCount().
  • getLargestPoolSize()Renvoie le plus grand nombre de threads ayant jamais été simultanément dans le pool. Le résultat renvoyé est inférieur ou égal à maximumPoolSize
  • getPoolSize()Renvoie le nombre actuel de threads dans le pool.
  • getActiveCount()Renvoie le nombre approximatif de threads qui exécutent activement des tâches.

 article de référence

  • L'art de la programmation concurrente Java
  • https://www.jianshu.com/p/87bff5cc8d8c
  • https://blog.csdn.net/programmer_at/article/details/79799267
  • https://blog.csdn.net/u013332124/article/details/79587436
  • https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice

Je suppose que tu aimes

Origine blog.csdn.net/a619602087/article/details/130527276
conseillé
Classement