thread java - comment utiliser correctement le pool de threads java

Les frameworks Java tels que Tomcat et Dubbo sont indissociables du pool de threads. Lorsque ces frameworks utilisent des threads, le pool de threads sera responsable. Lorsque nous utilisons ces frameworks, nous définissons les paramètres du pool de threads pour améliorer les performances. Alors, combien de threads sont appropriés ? Aujourd'hui, nous allons en apprendre davantage sur le pool de threads autour de ce problème.

Pourquoi utiliser un pool de threads

Habituellement, lorsque nous utilisons des threads Java, nous créons Threaddirectement un objet. La création et la destruction de threads Java impliquent Threadla création et la destruction d'objets, la commutation de threads et d'autres problèmes. Créer Threadun objet consiste simplement à allouer un bloc de mémoire dans le tas JVM ; pour créer un thread, vous devez appeler l'API du noyau du système d'exploitation, puis le système d'exploitation doit allouer une série de ressources pour le thread, ce qui est très cher. Ainsi, le fil est un objet lourd, la création et la destruction fréquentes doivent être évitées.

Généralement, les problèmes mentionnés ci-dessus peuvent être résolus grâce à l'idée de "pooling", et l'implémentation du pool de threads fourni dans le JDK est basée sur ThreadPoolExecutor.

L'utilisation d'un pool de threads peut apporter une série d'avantages :

  • Réduisez la consommation de ressources : réutilisez les threads créés grâce à la technologie de pooling pour réduire les pertes causées par la création et la destruction de threads.
  • Réactivité améliorée : Lorsqu'une tâche arrive, elle s'exécute immédiatement sans attendre la création du thread.
  • Améliorer la gérabilité des threads : les threads sont des ressources rares. S'ils sont créés sans limite, cela consommera non seulement des ressources système, mais conduira également à une planification déséquilibrée des ressources en raison d'une distribution déraisonnable des threads et réduira la stabilité du système. Utilisez le pool de threads pour une allocation, un réglage et une surveillance unifiés.
  • Fournir des fonctions de plus en plus puissantes : Le pool de threads est évolutif, permettant aux développeurs d'y ajouter plus de fonctions. Par exemple, le pool de threads de temporisation différée ScheduledThreadPoolExecutorpermet aux tâches d'être différées ou exécutées périodiquement.

Conception et implémentation du noyau du pool de threads

conception générale

pool de threads diagramme de classes uml.png

  • L'interface de niveau supérieur est Executor, java.util.concurrent.Executor#execute, l'utilisateur n'a qu'à fournir Runnablel'objet, soumettre la logique d'exécution de la tâche à l'exécuteur ( Executor), et Executorle framework terminera l'allocation des threads et l'exécution de la tâche.

  • ExecutorServiceL'interface étend Executoret ajoute quelques fonctionnalités :

    • Développez la possibilité d'exécuter des tâches et de générer des méthodes Future pour une ou un lot de tâches asynchrones en appelant des méthodes submit()ou ;invokeAll()
    • Fournit des méthodes pour gérer et contrôler le pool de threads, telles que l'appel shutdown()et d'autres méthodes pour arrêter le fonctionnement du pool de threads.
  • AbstractExecutorServiceC'est la classe abstraite de la couche supérieure, qui connecte le processus d'exécution des tâches en série, garantissant que l'implémentation de la couche inférieure ne doit se concentrer que sur une seule méthode d'exécution des tâches.

  • La classe d'implémentation spécifique est que d'une part ThreadPoolExecutor, ThreadPoolExecutorelle maintiendra son propre cycle de vie, et d'autre part, elle gérera les threads et les tâches en même temps, afin que les deux puissent être bien combinés pour exécuter des tâches parallèles.

  • ScheduledThreadPoolExecutorThreadPoolExecutorEt l'interface est étendue ScheduledExecutorServiceet la capacité de planification est augmentée, de sorte que la tâche peut être retardée et exécutée régulièrement.

  • Il existe également une classe qui fournit une méthode de fabrique pour la création de pool de threads Executorsafin de créer un pool de threads.

Ce chapitre explique principalement ThreadPoolExecutorle principe de mise en œuvre, qui ScheduledThreadPoolExecutorsera abordé dans le prochain article.

Principe d'implémentation de ThreadPoolExecutor

Description des paramètres de construction de ThreadPoolExecutor

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 
​
  • corePoolSize : Indique le nombre minimum de threads détenus par le pool de threads. Le nombre de core threads, une fois créés, ces core threads ne seront pas détruits. Au contraire, s'il s'agit d'un thread non principal, il sera détruit après l'exécution de la tâche et n'a pas été utilisé depuis longtemps.

  • maximumPoolSize : Indique le nombre maximum de threads créés par le pool de threads.

  • keepAliveTime&unit:一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTimeunit就是用来定义这个一段时间的参数。也就是说,如果线程已经空闲了keepAliveTimeunit这么久了,而且线程数大于corePoolSize,那么这个空闲线程就要被回收。

  • workQueue:用来存储任务,当有新的任务请求线程处理时,如果核心线程池已满,那么新来的任务会加入workQueue队列中,workQueue是一个阻塞队列。

  • threadFactory:通过这个参数可以自定义如何创建线程。

  • handler:通过这个参数可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,可以通过这个参数来指定

    ThreadPoolExecutor已经提供了四种策略。

    1. CallerRunsPolicy:提交任务的线程自己去执行该任务。
    2. AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException.
    3. DiscardPolicy:直接丢弃任务,没有任何异常输出。
    4. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

ThreadPoolExecutor执行流程

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}
  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

Processus d'exécution du pool de threads.png

线程池运行状态

线程池的运行状态,由线程池内部维护,线程池内部使用AtomicInteger变量,用于维护运行状态runState和工作线程数workerCount,高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
​
// COUNT_BITS=29,(对于int长度为32来说)表示线程数量的字节位数
private static final int COUNT_BITS = Integer.SIZE - 3;
// 状态掩码,高三位是1,低29位全是0,可以通过 ctl&COUNT_MASK 运算来获取线程池状态
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
​
​
private static final int RUNNING    = -1 << COUNT_BITS; // 111 00000 00000000 00000000 00000000;
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 000 00000 00000000 00000000 00000000; 
private static final int STOP       =  1 << COUNT_BITS; // 001 00000 00000000 00000000 00000000;
private static final int TIDYING    =  2 << COUNT_BITS; // 010 00000 00000000 00000000 00000000;
private static final int TERMINATED =  3 << COUNT_BITS; // 011 00000 00000000 00000000 00000000;// 计算当前运行状态
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
// 计算当前线程数量
private static int workerCountOf(int c)  { return c & COUNT_MASK; }
//通过状态和线程数生成ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }
状态 描述
RUNNING 能接受新的任务,也能处理阻塞队列中的任务
SHUTDOWN 关闭状态,不能接受新的任务,只能处理阻塞队列中的任务
STOP 不能接受新的任务,也不能处理阻塞队列中的任务,会中断正在处理任务的线程
TIDYING 所有任务都停止了,workerCount为0
TERMINATED 在执行terminated()方法会进入到这个状态

状态转移:

Pool de threads en cours d'exécution transfer.png

阻塞队列

再介绍线程池总体设计的时候,说过线程池的设计,采用的都是生产者 - 消费者模式,其实现主要就是通过BlockingQueue来实现的,目的是将任务和线程两者解耦,阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

阻塞队列 描述
ArrayBlockingQueue 基于数组实现的有界队列,支持公平锁和非公平锁
LinkedBlockingQueue 基于链表实现的有界队列,队列大小默认为Integer.MAX_VALUE,所以默认创建该队列会有容量危险
PriorityBlockingQueue 支持优先级排序的无界队列,不能保证同优先级的顺序
DelayQueue 基于PriorityBlockingQueue实现的延期队列,只有当延时期满了,才能从中取出元素
SynchronousQueue 同步队列,不存储任何元素,调用一次put()就必须等待take()调用完。支持公平锁和非公平锁
LinkedTransferQueue 基于链表实现的无界队列,多了transfer()tryTransfer()方法
LinkedBlockingDeque 基于双向链表实现的队列,多线程并发时,可以将锁的竞争最多降到一半

Worker

Worker整体设计

  • Worker继承了AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。
  • Worker实现了Runnable接口,持有一个线程thread,一个初始化的任务firstTaskthread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;//Worker持有的线程
    Runnable firstTask;//初始化的任务,可以为null
  
    Worker(Runnable firstTask) {
      setState(-1); // inhibit interrupts until runWorker
      this.firstTask = firstTask;
      this.thread = getThreadFactory().newThread(this);
    }
  
    public void run() {
      runWorker(this);
    }
  
  // ...省略其余代码
}

Worker如何添加任务

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (int c = ctl.get();;) {
        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;
​
        for (;;) {
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                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 c = ctl.get();
​
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWorker()方法有两个参数:

  • firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行workQueue中的任务,也就是非核心线程的创建。
  • core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSizefalse表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize

具体流程如下:

Pool de threads addWorker execution process.png

Worker如何获取任务

任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。

第一种在上述addWorker()方法中,如果firstTask不为空的话,会直接运行。第二种firstTask为空,任务将从workQueue中获取,调用getTask()方法

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
​
        for (;;) {
            int c = ctl.get();
            // Check if queue empty only if necessary.
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, 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;
            }
        }
    }

具体流程:

Pool de threads getTask() processus d'exécution.png

Worker如何运行任务

// java.util.concurrent.ThreadPoolExecutor#runWorker
final void runWorker(Worker w) {
  Thread wt = Thread.currentThread();
  Runnable task = w.firstTask;
  w.firstTask = null;
  w.unlock(); // allow interrupts
  boolean completedAbruptly = true;
  try {
    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);
        try {
          task.run();
          afterExecute(task, null);
        } catch (Throwable ex) {
          afterExecute(task, ex);
          throw ex;
        }
      } finally {
        task = null;
        w.completedTasks++;
        w.unlock();
      }
    }
    completedAbruptly = false;
  } finally {
    processWorkerExit(w, completedAbruptly);
  }
}

具体流程:

Pool de threads runWorker execution process.png

  1. while循环不断地通过getTask()方法获取任务。
  2. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
  3. 执行任务。
  4. 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

Worker线程如何回收

线程的销毁依赖JVM自动的回收,但线程池中核心线程是不能被jvm回收的,所以当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

其主要逻辑在processWorkerExit()方法中

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();
​
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
​
    tryTerminate();
​
    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

具体流程:

Pool de threads processWorkerExit execution process.png

使用线程池最佳实践

Executors

考虑到ThreadPoolExecutor的构造函数实现有些复杂,所以java提供了一个线程池的静态工厂类,Executors,利用Executors可以快速创建线程池。但是大厂都不建议使用Executors,原因:Executors的很多方法默认使用的是无参构造的LinkedBlockQueue,默认大小为Integer.MAX_VALUE,高负载情况下,队列很容易导致OOM。而OOM了就会导致所有请求都无法处理。强烈建议使用ArrayBlockingQueue有界队列。

使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会throw RejectedExecutionException这个运行时异常,所以开发人员很容易忽略,因此默认拒绝策略需要慎重使用。如果线程处理的任务非常重要,建议自定义拒绝策略,实际开发中,自定义拒绝策略往往和降级策略配合使用。

下面介绍常用的方法:

newFixedThreadPool()

  • newFixedThreadPool()函数用来创建大小固定的线程池。
  • ThreadPoolExecutor中的maximumPoolSizecorePoolSize相等,因此,线程池中的线程都是核心线程,一旦创建便不会销毁。
  • workQueue为LinkedBlockingQueue,默认大小为Integer.MAX_VALUE,大小非常大,相当于无界阻塞队列。任务可以无限的往workQueue中提交,永远都不会触发拒绝策略。
public static ExecutorService newFixedThreadPool(int nThreads) {
  return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor()

  • newSingleThreadExecutor()函数用来创建单线程执行器。
  • ThreadPoolExecutor中的maximumPoolSizecorePoolSize都等于1。
  • workQueue同样是大小为Integer.MAX_VALUELinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

newCachedThreadPool()

  • newCachedThreadPool()函数创建的线程池只包含非核心线程,线程空闲60秒以上便会销毁。

  • workQueueSynchronousQueue类型的,而SynchronousQueue是长度为0的阻塞队列,所以,workQueue不存储任何等待执行的任务。

    • 如果线程池内存在空闲线程,那么新提交的任务会被空闲线程执行
    • 如果线程池内没有空闲线程,那么线程池会创建新的线程来执行新提交的任务。
  • 线程池大小为Integer.MAX_VALUE,因此,线程池中创建的线程个数可以非常多。

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

异常捕获

Lorsque vous utilisez le pool de threads, vous devez également faire attention au problème de gestion des exceptions. Lors de l'exécution de tâches via des méthodes d'objet, si une exception d'exécution se produit pendant l'exécution de la tâche, le thread de la tâche se terminera, mais vous ne recevrez aucune notification , ce qui ThreadPoolExecutorvous execute()Bien que le pool de threads fournisse de nombreuses méthodes de gestion des exceptions, la solution la plus sûre et la plus simple consiste à capturer les informations sur les exceptions et à les traiter à la demande.

Configurer les paramètres 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.

Voir Paramétrage dynamique github.com/shawn-happy…

documents de référence

Pool de threads Meituan

Je suppose que tu aimes

Origine juejin.im/post/7255939146374725693
conseillé
Classement