En cuanto al grupo de subprocesos, debes saber estas cosas

Prólogo

Hola a todos, soy Jack Xu. Esta es la segunda parte de la programación concurrente. Hoy les hablaré sobre el grupo de subprocesos. Este artículo es un poco largo y los niños lo leen con calma y paciencia. .

¿Por qué usar el grupo de subprocesos?

1) Reduzca la sobrecarga de rendimiento de crear y destruir hilos

2) Mejore la velocidad de respuesta: cuando se ejecuta una nueva tarea, se puede ejecutar inmediatamente sin esperar a que se cree el hilo.

3) Establecer razonablemente el tamaño del grupo de subprocesos puede evitar problemas causados ​​por el número de subprocesos que exceden el cuello de botella de recursos de hardware

Echemos un vistazo a la especificación de código de Alibaba. La creación de subprocesos en un proyecto debe crearse utilizando un grupo de subprocesos. La razón es que dije los tres puntos anteriores

Uso del grupo de subprocesos

Primero echemos un vistazo al diagrama de clase UML

  • Ejecutor: Puede ver que la capa superior es la interfaz Ejecutor. Esta interfaz es muy simple, con un solo método de ejecución. El propósito de esta interfaz es desacoplar el envío de tareas y la ejecución de tareas.

  • ExecutorService: sigue siendo una interfaz, heredada de Executor, que amplía la interfaz de Executor y define más operaciones relacionadas con el grupo de subprocesos.

  • AbstractExecutorService: proporciona algunas implementaciones predeterminadas de ExecutorService.

  • ThreadPoolExecutor: en realidad, la implementación del grupo de subprocesos que utilizamos es ThreadPoolExecutor. Implementa un mecanismo completo para el trabajo del grupo de subprocesos. También es el foco de nuestro próximo análisis.

  • ForkJoinPool: ThreadPoolExecutor y ThreadPoolExecutor se heredan de AbstractExecutorService, adecuado para dividir y conquistar, algoritmo de cálculo recursivo

  • ScheduledExecutorService: esta interfaz extiende ExecutorService para definir un método para la ejecución retrasada y la ejecución periódica de tareas.

  • ScheduledThreadPoolExecutor: esta interfaz implementa la interfaz ScheduledExecutorService sobre la base de heredar ThreadPoolExecutor, proporcionando las características de ejecución de tareas periódicas y periódicas.

Es importante comprender la estructura anterior. Executors es una clase de herramienta, y luego busca dos formas de crear hilos. La primera es implementarla a través de los métodos de fábrica proporcionados por Executors. Hay cuatro maneras

        Executor executor1 = Executors.newFixedThreadPool(10);
        Executor executor2 = Executors.newSingleThreadExecutor();
        Executor executor3 = Executors.newCachedThreadPool();
        Executor executor4 = Executors.newScheduledThreadPool(10);
复制代码

El segundo es a través del método de construcción.

        ExecutorService executor5 = new ThreadPoolExecutor(1,
                1,
                0L,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
复制代码

De hecho, mire el código fuente creado de la primera manera y encontrará:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }
复制代码

Básicamente, al llamar al constructor de ThreadPoolExecutor, se pasan diferentes parámetros durante la creación, por lo que esencialmente solo hay una forma de crear un grupo de subprocesos, que es usar el constructor. Aquí no quiero hablar sobre cómo el método de fábrica de los Ejecutores nos ayudó específicamente a crear Grupo de subprocesos, veamos otra especificación de Alibaba.

Todo el mundo entiende aquí, es porque la encapsulación es demasiado fuerte, pero los chicos no sabrán cómo usarla, el mal uso, el abuso, puede conducir a OOM, a menos que esté familiarizado con los cuatro grupos de hilos creados, por lo que Lo introduje en vano, porque no se usa. A continuación, nos centraremos en el significado de cada parámetro en el método de construcción ThreadPoolExecutor. Hay muchos métodos de construcción. Elegí el más completo.

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                          int maximumPoolSize, //最大线程数
                          long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
                          TimeUnit unit, //存活时间单位
                          BlockingQueue<Runnable> workQueue, //保存执行任务的队列
                          ThreadFactory threadFactory,//创建新线程使用的工厂
                          RejectedExecutionHandler handler //当任务无法执行的时候的处理方式)
复制代码
  • corePoolSize: el número de subprocesos principales en el grupo de subprocesos, de hecho, el número mínimo de subprocesos. Sin allowCoreThreadTimeOut, los hilos dentro del número de hilos principales siempre sobrevivirán. El subproceso no se destruye a sí mismo, pero vuelve al grupo de subprocesos en un estado suspendido. Hasta que la aplicación envíe una solicitud al grupo de subprocesos nuevamente, el subproceso suspendido en el grupo de subprocesos activará nuevamente la tarea de ejecución.

  • maximumPoolSize: el número máximo de subprocesos en el grupo de subprocesos

  • keepAliveTime and unit: el tiempo de supervivencia y la unidad después de exceder el número de hilos centrales

  • workQueue: es una cola de bloqueo, utilizada para guardar todas las tareas que debe realizar el grupo de subprocesos. Los siguientes tres tipos están generalmente disponibles:

1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;  
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
复制代码
  • ThreadFactory: generalmente utilizamos los Executors.defaultThreadFactory () predeterminados de fábrica. ¿Por qué usar una fábrica? De hecho, es para regular el subproceso generado. Evite llamar a la creación de nuevos subprocesos, lo que puede causar diferencias en el subproceso creado

  • manejador: estrategia de saturación después de que la cola y el grupo de subprocesos más grande estén llenos.

1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录
日志或持久化存储不能处理的任务
复制代码

Después de crear el grupo de subprocesos, también es muy fácil de usar, con valor de retorno y sin valor de retorno, correspondiente a la implementación de la interfaz Runnable o Callable correspondiente

        //无返回值
        executor5.execute(() -> System.out.println("jack xushuaige"));
        //带返回值
        String message = executor5.submit(() -> { return "jack xushuaige"; }).get();
复制代码

Análisis de código fuente

método de ejecución

Basado en la entrada del código fuente para el análisis, primero mire el método de ejecución

    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);
    }
复制代码

Hay un comentario clave en el código fuente que no pegué. Permítanme explicar primero la traducción de este comentario clave:

Se procesa en tres pasos:

1. Si el número de subprocesos en ejecución es menor que corePoolSize, intente crear un nuevo subproceso y ejecutar el comando entrante como su primera tarea. Llamar a addWorker verificará automáticamente runState y workCount, para evitar la advertencia de error de agregar un subproceso cuando el subproceso no debe agregarse;

2. Incluso si la tarea se puede agregar con éxito a la cola, aún debemos confirmar nuevamente si debemos agregar el hilo (porque puede haber hilos muertos después de la última verificación) o si el grupo de hilos se ha detenido después de ingresar este método. Por lo tanto, verificaremos el estado nuevamente y revertiremos la cola si es necesario. O cuando no hay hilo, comience un nuevo hilo;

3. Si la tarea no se puede agregar a la cola, puede intentar agregar un nuevo hilo. Si la adición falla, es porque el grupo de subprocesos está cerrado o saturado, por lo que la tarea se rechaza.

Si todavía pareces tonto después de leer, entonces está bien. Dibujaré este diagrama de flujo.

Luego introduzca qué ctl está en el código fuente, haga clic aquí para ver el código fuente

Descubrimos que es una clase atómica cuya función principal es guardar el número de subprocesos y el estado del grupo de subprocesos. Utiliza operaciones de bits. Un valor int es de 32 bits. Aquí, los 3 bits superiores se usan para guardar el estado de ejecución, y los 29 bits inferiores Para guardar el número de hilos.

Calculemos el método ctlOf (RUNNING, 0), donde RUNNING = -1 << COUNT_BITS; -1 se desplaza a la izquierda 29 bits, y el binario de -1 es 32 1s (1111 1111 1111 1111 1111 1111 1111 1111), se desplaza a la izquierda 29 Después de obtener el bit (1110 0000 0000 0000 0000 0000 0000 0000), entonces 111 | 0 o 111, de manera similar, puede obtener el bit de otros estados. Esta operación de bits es muy interesante. La operación de bits también se usa en el código fuente del mapa hash. Los chicos también pueden intentar usarla en el desarrollo habitual, de modo que la velocidad de operación sea rápida, y se puede instalar con b. Introduzca el estado de estos cinco grupos de subprocesos

  • EN EJECUCIÓN: reciba nuevas tareas y ejecute tareas en la cola

  • APAGADO: no reciba nuevas tareas, pero ejecute tareas en la cola

  • DETENER: no reciba nuevas tareas, no ejecute las tareas en la cola, interrumpa las tareas en progreso

  • TIDYING: todas las tareas han finalizado, el número de subprocesos es 0, el grupo de subprocesos en este estado está a punto de llamar al método terminado ()

  • TERMINADO: la ejecución del método Terminated () se ha completado

Su relación de conversión es la siguiente:

método addWorker

Vemos que el método principal del proceso de ejecución es addWorker, seguimos analizando, el código fuente parece falso, en realidad hizo dos cosas, lo dividió

El primer paso: actualizar el número de trabajadores, el código es el siguiente:

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
            }
        }
复制代码

Reintentar es una marca, que se usa junto con el bucle, cuando continúa reintentando, saltará al lugar de reintento y se ejecutará nuevamente. Si vuelve a intentarlo, salte del cuerpo completo del bucle. El código fuente primero obtiene CTL, y luego verifica el estado, y luego verifica la cantidad de acuerdo con el tipo de hilo creado. Después de actualizar el estado de ctl a través de CAS, si tiene éxito, saltará del bucle. De lo contrario, el estado del grupo de subprocesos se obtiene nuevamente. Si no es coherente con el original, la ejecución comenzará desde el principio. Si el estado no ha cambiado, continúe actualizando el número de trabajadores. El diagrama de flujo es el siguiente:

Paso 2: Agregar trabajadores al conjunto de trabajadores. Y comience el hilo sostenido en el trabajador. El código es el siguiente:

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();
            workerStarted = true;
        }
    }
} finally {
    if (! workerStarted)
        addWorkerFailed(w);
}
return workerStarted;
复制代码

Puede ver que cuando agrega un trabajo, primero necesita obtener el bloqueo, para garantizar la seguridad de la concurrencia de subprocesos múltiples. Si el trabajador se agrega correctamente, se llama al método de inicio del subproceso en el trabajador para iniciar el subproceso. Si el inicio falla, llame al método addWorkerFailed para revertir. Cuando veas a este chico, encontrarás

1. ThreadPoolExecutor no se inicia y crea ningún subproceso después de la inicialización, se llamará a addWorker para crear subprocesos cuando se llame al método de ejecución

2. En el método addWorker, se crea un nuevo trabajador y se inicia el subproceso que contiene para realizar la tarea.

Como se mencionó anteriormente, si el número de subprocesos ha alcanzado corePoolSize, solo el comando se agregará a workQueue, entonces, ¿cómo se ejecuta el comando agregado a workQueue? Analicemos el código fuente de Worker.

Clase trabajadora

El trabajador encapsula el hilo y es la unidad de trabajo en el ejecutor. El trabajador hereda de AbstractQueuedSynchronizer e implementa Runnable. La simple comprensión del trabajador es en realidad un hilo, y el método de ejecución se ha recreado. Veamos su método de construcción:

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
复制代码

Echemos un vistazo a estos dos atributos importantes

        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
复制代码

firstTask lo usa para guardar tareas entrantes; el hilo es el hilo creado por ThreadFactory al llamar al constructor, es el hilo usado para procesar la tarea, aquí está el hilo creado por ThreadFactory, y no hay ninguna novedad directa, la razón también se menciona anteriormente He estado aquí. Aquí vemos que esto es pasado por newThread. Debido a que el propio trabajador hereda la interfaz Runnable, el t.start () llamado en addWork realmente ejecuta el método de ejecución del trabajador al que pertenece t. El método de ejecución del trabajador es el siguiente:

public void run() {
    runWorker(this);
}
复制代码

El código fuente de runWorker es el siguiente:

    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);
                    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);
        }
    }
复制代码

Análisis simple

1. Primero quite la primera Tarea del trabajador y límpiela;

2. Si no hay firstTask, llame al método getTask para obtener la tarea de workQueue;

3. Adquirir la cerradura;

4. Ejecute beforeExecute. Este es un método vacío, que se implementa en subclases si es necesario;

5. Ejecute task.run;

6. Ejecute afterExecute. Este es un método vacío, que se implementa en subclases si es necesario;

7. Borrar la tarea, completar Tareas ++ y liberar el bloqueo;

8. Cuando hay una excepción o ninguna tarea es ejecutable, ingresará el bloque de código de finnaly externo. Llame a processWorkerExit para salir del trabajador actual. Después de eliminar a este trabajador de las obras, si el número de trabajadores es menor que corePoolSize, cree un nuevo trabajador para mantener el número de subprocesos corePoolSize.

Esta línea de código mientras (task! = Null || (task = getTask ())! = Null) asegura que el trabajador sigue obteniendo la ejecución de tareas de workQueue. El método getTask saldrá de la encuesta o tomará tareas en BlockingQueue workQueue.

En este punto, el proceso de cómo el ejecutor crea y comienza el hilo para ejecutar la tarea se ha analizado claramente, y existen otros métodos como shutdown (), shutdownNow () para que los niños observen y estudien por sí mismos.

Cómo configurar correctamente el tamaño del grupo de subprocesos

El tamaño del grupo de subprocesos no depende de adivinar, ni significa que cuanto más mejor.

  • Tareas intensivas en CPU: principalmente para realizar tareas informáticas, el tiempo de respuesta es rápido, la CPU se ha estado ejecutando, esta tarea tiene una alta utilización de la CPU, luego la configuración del número de subprocesos debe determinarse de acuerdo con el número de núcleos de CPU, y deben asignarse menos subprocesos , Como el tamaño equivalente al número de CPU.
  • Tareas intensivas de E / S: principalmente para operaciones de E / S, y el tiempo para realizar operaciones de E / S es largo. Dado que el subproceso no se ejecuta todo el tiempo, la CPU está inactiva. En este caso, el tamaño del grupo de subprocesos se puede aumentar, como la cantidad de CPU * 2

Por supuesto, todos estos son valores empíricos, y la mejor manera es probar y obtener la mejor configuración de acuerdo con la situación real.

Monitoreo de grupo de subprocesos

Si se usa un grupo de subprocesos a gran escala en el proyecto, entonces debe existir un sistema de monitoreo para guiar el estado actual del grupo de subprocesos, y los problemas se pueden ubicar rápidamente cuando surgen problemas. Podemos lograr la supervisión de subprocesos reescribiendo los métodos beforeExecute, afterExecute y shutdown del grupo de subprocesos

Puede ver a partir de estos nombres y definiciones que esto se implementa mediante subclases, que pueden ejecutar una lógica personalizada antes, después y después de que se ejecute el subproceso.

Resumen

El grupo de subprocesos es simple y fácil de decir, y difícil de decir. Es simple porque es fácil de usar, por lo que los chicos pueden pensar que no hay nada que decir sobre esto. La dificultad es conocer su código fuente subyacente y cómo programa los subprocesos. Sí, hablemos de dos cosas. La primera es que se usan muchos diagramas de flujo en este artículo. Cuando leemos el código fuente o hacemos un complejo desarrollo de negocios, debemos calmarnos y hacer un dibujo primero, de lo contrario, será un halo u otros Después de la interrupción, tengo que mirar a un lado desde el principio hasta el final. El segundo es leer el código fuente. El socio recién graduado puede usarlo mientras funcione, pero si ha estado trabajando durante cinco años, solo lo usará. No sabe lo que es. Cómo lograrlo, cuál es su ventaja sobre el nuevo graduado y qué salario es más alto que el nuevo graduado. Bueno, los autores de este artículo son de nivel limitado. Si tiene alguna pregunta, comparta y discuta ...

Supongo que te gusta

Origin juejin.im/post/5e906cc2e51d4546e8576569
Recomendado
Clasificación