subproceso de java: cómo usar correctamente el grupo de subprocesos de java

Los marcos Java como Tomcat y Dubbo son inseparables del grupo de subprocesos. Cuando estos marcos utilizan subprocesos, el grupo de subprocesos será el responsable. Cuando usamos estos marcos, estableceremos parámetros de grupo de subprocesos para mejorar el rendimiento. Entonces, ¿cuántos hilos son apropiados? Hoy aprenderemos sobre el grupo de subprocesos en torno a este problema.

¿Por qué usar un grupo de subprocesos?

Por lo general, cuando usamos subprocesos de Java, creamos un Threadobjeto directamente. La creación y destrucción de subprocesos de Java implicará Threadla creación y destrucción de objetos, el cambio de subprocesos y otros problemas. Crear Threadun objeto es solo asignar un bloque de memoria en el montón de JVM; para crear un subproceso, debe llamar a la API del kernel del sistema operativo, y luego el sistema operativo debe asignar una serie de recursos para el subproceso, que es muy caro. Por lo tanto, el hilo es un objeto pesado, se debe evitar la creación y destrucción frecuentes.

En general, los problemas mencionados anteriormente se pueden resolver mediante la idea de "agrupación", y la implementación del grupo de subprocesos proporcionado en el JDK se basa en ThreadPoolExecutor.

El uso de un grupo de subprocesos puede traer una serie de beneficios:

  • Reduzca el consumo de recursos : reutilice los subprocesos creados a través de la tecnología de agrupación para reducir la pérdida causada por la creación y destrucción de subprocesos.
  • Capacidad de respuesta mejorada : cuando llega una tarea, se ejecuta inmediatamente sin esperar a que se cree un hilo.
  • Mejore la capacidad de administración de los subprocesos : los subprocesos son recursos escasos. Si se crean sin límite, no solo consumirán recursos del sistema, sino que también conducirán a una programación de recursos desequilibrada debido a la distribución irrazonable de subprocesos y reducirán la estabilidad del sistema. Utilice el grupo de subprocesos para la asignación, el ajuste y la supervisión unificados.
  • Proporcione funciones cada vez más potentes : el grupo de subprocesos es escalable, lo que permite a los desarrolladores agregarle más funciones. Por ejemplo, el grupo de subprocesos de temporización retrasada ScheduledThreadPoolExecutorpermite que las tareas se difieran o se ejecuten periódicamente.

Diseño e implementación del núcleo del grupo de subprocesos

diseño general

grupo de subprocesos diagrama de clase uml.png

  • La interfaz de nivel superior es Executor, java.util.concurrent.Executor#execute, el usuario solo necesita proporcionar Runnableel objeto, enviar la lógica de ejecución de la tarea al ejecutor ( Executor), y Executorel marco completará la asignación de subprocesos y la ejecución de la tarea.

  • ExecutorServiceLa interfaz amplía Executory añade algunas capacidades:

    • Amplíe la capacidad de ejecutar tareas y genere métodos futuros para una o un lote de tareas asincrónicas mediante llamadas submit()o métodos;invokeAll()
    • Proporciona métodos para administrar y controlar el grupo de subprocesos, como llamadas shutdown()y otros métodos para detener el funcionamiento del grupo de subprocesos.
  • AbstractExecutorServiceEs la clase abstracta de la capa superior, que conecta el proceso de ejecución de tareas en serie, lo que garantiza que la implementación de la capa inferior solo necesite centrarse en un método de ejecución de tareas.

  • La clase de implementación específica es que, por un lado ThreadPoolExecutor, ThreadPoolExecutormantendrá su propio ciclo de vida, y por otro lado, administrará hilos y tareas al mismo tiempo, por lo que los dos pueden combinarse bien para ejecutar tareas paralelas.

  • ScheduledThreadPoolExecutorThreadPoolExecutorY la interfaz se expande ScheduledExecutorServicey la capacidad de programación aumenta, de modo que la tarea se puede retrasar y ejecutar regularmente.

  • También hay una clase que proporciona un método de fábrica para la creación de grupos de subprocesos Executorspara crear un grupo de subprocesos.

Este capítulo explica principalmente ThreadPoolExecutorel principio de implementación, que ScheduledThreadPoolExecutorserá discutido en el siguiente artículo.

Principio de implementación de ThreadPoolExecutor

Descripción de los parámetros de construcción de ThreadPoolExecutor

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 
​
  • corePoolSize : indica el número mínimo de subprocesos que tiene el grupo de subprocesos. El número de subprocesos principales, una vez creados, estos subprocesos principales no se destruirán. Por el contrario, si es un subproceso no central, se destruirá después de que se ejecute la tarea y no se haya utilizado durante mucho tiempo.

  • MaximumPoolSize : indica el número máximo de subprocesos creados por el grupo de subprocesos.

  • 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,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

Proceso de ejecución del grupo de subprocesos.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()方法会进入到这个状态

状态转移:

Transferencia de estado de ejecución del grupo de subprocesos.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

具体流程如下:

Grupo de subprocesos addWorker proceso de ejecución.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;
            }
        }
    }

具体流程:

Grupo de subprocesos getTask() proceso de ejecución.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);
  }
}

具体流程:

Grupo de subprocesos runWorker proceso de ejecución.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);
    }
}

具体流程:

Grupo de subprocesos processWorkerExit proceso de ejecución.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>());
}

异常捕获

Al usar el grupo de subprocesos, también debe prestar atención al problema del manejo de excepciones. Al ejecutar tareas a través de métodos de objetos, si se produce una excepción en tiempo de ejecución durante la ejecución de la tarea, el subproceso de la tarea finalizará, pero no recibirá ninguna notificación. , lo que ThreadPoolExecutorle execute()Aunque el grupo de subprocesos proporciona muchos métodos para el manejo de excepciones, la solución más segura y sencilla es capturar información de excepción y procesarla a pedido.

Configurar parámetros de grupo de subprocesos

Desde las cuatro perspectivas de prioridad de la tarea, tiempo de ejecución de la tarea, naturaleza de la tarea (uso intensivo de CPU/uso intensivo de E/S) y dependencias de tareas. Y use colas de trabajo limitadas lo más cerca posible.

Las tareas de diferente naturaleza se pueden procesar por separado utilizando grupos de subprocesos de diferentes tamaños:

  • Uso intensivo de la CPU: la menor cantidad posible de subprocesos, Ncpu+1
  • Uso intensivo de E/S: tantos subprocesos como sea posible, Ncpu*2, como el grupo de conexiones de la base de datos
  • Híbrido: las tareas con uso intensivo de CPU y las tareas con uso intensivo de E/S tienen poca diferencia en el tiempo de ejecución y se dividen en dos grupos de subprocesos; de lo contrario, no hay necesidad de dividirse.

Ver Parametrización dinámica github.com/shawn-happy…

documentos de referencia

Grupo de subprocesos de Meituan

Supongo que te gusta

Origin juejin.im/post/7255939146374725693
Recomendado
Clasificación