[Notas de estudio de Java: programación concurrente] conjunto de subprocesos

prefacio

En el artículo anterior, resolvimos los conceptos de subprocesos y tareas de Java. Este artículo habla sobre el conjunto de subprocesos de Java. Si bien comprendemos mejor el conjunto de subprocesos, también agregamos algunas ideas de diseño.

Portal:

[Notas de estudio de Java: conceptos básicos] Acerca de los subprocesos múltiples de Java: subprocesos y tareas

1. El surgimiento del conjunto de subprocesos de Java

Si ahora necesitamos optimizar el rendimiento de los escenarios simultáneos, ¿por dónde deberíamos empezar? Obviamente, el ángulo más simple: ahorrar la sobrecarga de creación y destrucción de subprocesos .

Ya sea que se trate de una aplicación con uso intensivo de E/S o una aplicación con uso intensivo de cómputo, la administración de subprocesos requiere un costo, por lo que, desde la perspectiva de la computadora, también debemos limitar cantidad de subprocesos . El propósito de crear subprocesos es hacer que la aplicación tenga un mejor rendimiento, y si hay demasiados subprocesos suspendidos, el costo de la administración de subprocesos es demasiado alto y el rendimiento de la aplicación también disminuirá.

Para los dos requisitos anteriores, nació el grupo de subprocesos.

Primero, déjame mostrarte la relación entre la interfaz y la clase de implementación en este artículo:

inserte la descripción de la imagen aquí

En segundo lugar, la interfaz básica del conjunto de subprocesos de Java

Ejecutor 与 ExecutorService

Esta es la interfaz básica de los dos grupos de subprocesos y ExecutorService hereda Executor. En general, hay más implementaciones usando ExecutorService porque tiene una especificación más completa. Eche un vistazo a estas dos interfaces primero:

public interface Executor {
    
    

    void execute(Runnable command);
    
}

Similar a la interfaz Runnable, la definición es muy simple, coloque la implementación de la tarea en el ejecutor y ejecute la tarea.

public interface ExecutorService extends Executor {
    
    

	//关闭控制类函数
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

	//运行类函数
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
	
	//批量运行类函数
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

Aquí, podemos ver el prototipo del grupo de subprocesos.Para administrar mejor los subprocesos, podemos realizar las funciones básicas requeridas por el grupo de subprocesos.

Aunque ExecutorService tiene más especificaciones que Executor, el resumen no es más que tres categorías:

  • Apagado y otras funciones de control (nivel de piscina)
  • Ejecutar funciones de clase (nivel de subproceso)
  • Función de tipo de operación por lotes (extensión del segundo tipo)

Aquí podemos prestar atención a estas dos interfaces:

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

¿Se siente familiar? Presione el botón aquí primero, y sabrá por qué está diseñado de esta manera en la siguiente sección.

3. Implementación básica del conjunto de subprocesos de Java

AbstractExecutorServiceAbstractExecutorService

Esta clase abstracta implementa la interfaz ExecutorService en etapas.En esta clase, se aclaran algunas ideas y pasos para implementar el grupo de subprocesos.

Primero, echemos un vistazo a la implementación básica de la tarea y la ejecución de una sola tarea:

  • El tipo de tarea básica del grupo de subprocesos es FutureTask
  • Enviar una función de tarea requiere dos pasos: crear un nuevo objeto de tarea -> el ejecutor ejecuta la tarea.
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    
    
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    
    
        return new FutureTask<T>(callable);
    }

    public Future<?> submit(Runnable task) {
    
    
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Runnable task, T result) {
    
    
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

Hasta ahora, entendemos por qué la función de envío parece tan familiar, porque sigue la misma lógica que FutureTask, por lo que también proporciona dos interfaces ordenadas.

A continuación, echemos un vistazo al método de procesamiento de la operación por lotes. invoqueCualquiera implica la programación de la gestión de subprocesos. Aquí, presionemos primero la tabla. Primero veamos la implementación en invocarTodo:

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    
    
        if (tasks == null)
            throw new NullPointerException();
        ArrayList<Future<T>> futures = new ArrayList<>(tasks.size());
        try {
    
    
            for (Callable<T> t : tasks) {
    
    
                RunnableFuture<T> f = newTaskFor(t);
                futures.add(f);
                execute(f);
            }
            for (int i = 0, size = futures.size(); i < size; i++) {
    
    
                Future<T> f = futures.get(i);
                if (!f.isDone()) {
    
    
                    try {
    
     f.get(); }
                    catch (CancellationException | ExecutionException ignore) {
    
    }
                }
            }
            return futures;
        } catch (Throwable t) {
    
    
            cancelAll(futures);
            throw t;
        }
    }

Se puede ver que la búsqueda de valores es un proceso de recorrer toda la lista, porque los resultados se obtienen en el orden de la lista, por lo que el tiempo de finalización está determinado por la tarea más lenta .

Hasta ahora, tenemos una comprensión preliminar del grupo de subprocesos, echemos un vistazo a cómo Java implementa un grupo de subprocesos completo.

ThreadPoolExecutor

En primer lugar, podemos echar un vistazo a los elementos mínimos necesarios para la inicialización del grupo de subprocesos a través del método de construcción .

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
    
    
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

Descripción de parámetros:

  • corePoolSize: el número de subprocesos principales en el grupo de subprocesos. Al agregar una tarea, si la cantidad actual de subprocesos (inactivos + no inactivos) es menor que corePoolSize, incluso si actualmente hay subprocesos inactivos, se crearán nuevos subprocesos para ejecutar la tarea. Si la cantidad actual de subprocesos (inactivos + no inactivos) es igual a corePoolSize, no se vuelven a crear más subprocesos.
  • MaximumPoolSize: el número máximo de subprocesos en el grupo de subprocesos. Si la cola de bloqueo está llena y el número actual de subprocesos (inactivos + no inactivos) es inferior o igual al tamaño máximo del grupo, se creará un nuevo subproceso para realizar la tarea.
  • keepAliveTime: si la cantidad actual de subprocesos (inactivos + no inactivos) es mayor que corePoolSize y el tiempo de inactividad es mayor que keepAliveTime, estos subprocesos inactivos se destruirán y se liberarán los recursos.
  • unidad: La unidad de tiempo.
  • workQueue: una instancia de una cola de bloqueo.
  • ThreadFactory: clase de fábrica de hilos, generalmente usa la fábrica predeterminada.
  • RejectedExecutionHandler: estrategia de saturación. Cuando hay demasiadas tareas, la estrategia de procesamiento para las tareas.
1. El estado del grupo de subprocesos: operación de bits

Para mejorar la eficiencia de la operación y reducir la sobrecarga, cuando registremos algunos valores de atributo, usaremos operaciones de bits, es decir, operaciones de bits. (Por ejemplo, si desea utilizar el árbol de Huffman para lograr la compresión en la clase de algoritmo, debe registrar varios datos de atributos del archivo de origen).

Si no tienes claro conceptos como operaciones con bits, puedes pasar a:

Explicación detallada de la operación de bit y la operación de cambio en Java

Echemos un vistazo a cómo ThreadPollExecutor usa un int para registrar dos atributos:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //能存放 32-3 位的活跃线程数量。
    private static final int COUNT_BITS = Integer.SIZE - 3;
    // 
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits(3 bits free)
    // 在高位存储线程池状态
    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 & ~COUNT_MASK; }
    private static int workerCountOf(int c)  {
    
     return c & COUNT_MASK; }
    private static int ctlOf(int rs, int wc) {
    
     return rs | wc; }
2. Modelo de consumo de producción - BlockingQueue (cola de bloqueo)

En ThreadPoolExecutor, la estructura de datos que gestiona las tareas de guardado es la cola de bloqueo. Entre ellos, la forma de producción es la oferta, y la forma de consumo es encuesta y toma.

public interface BlockingQueue<E> extends Queue<E> {
    
    

	//……………………

    boolean offer(E e);

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
	//如果取出元素后,队列为空,一直阻塞等待。
    E take() throws InterruptedException;
    //如果取出元素后,队列为空,则阻塞超时后返回null。
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

	//……………………
	
}

En la especificación de la interfaz, es fácil ver la diferencia entre sondear y tomar. Eche un vistazo a la aplicación en el grupo de subprocesos:

    public void execute(Runnable command) {
    
    
		//……………………
        //向队列生产任务
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            //……………………
        }
        //……………………
    }

    private Runnable getTask() {
    
    
			//……………………
			boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            try {
    
    
            //依据元素是否是具备时间(timed)属性,而选择消费任务的方式。
                Runnable r = timed ?
                	//如果queue is null,即没有可执行任务,则阻塞超时后 return null。
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
    
    
                timedOut = false;
            }
        }
    }

Sugerencias:
elija la forma de consumir tareas en función de si el elemento tiene el atributo temporizado.
Si es un método de consumo de sondeo, el elemento tiene un atributo de tiempo de espera (consulte el código fuente de BlockingQueue para obtener más detalles) y se bloqueará en el valor hasta que se alcance el tiempo de espera (inactivo) y devolverá un valor nulo. ), llame al método de destrucción de subprocesos processWorkerExit , para lograr: cuando la cantidad de subprocesos > corePoolSize, los subprocesos inactivos se reciclan a tiempo.
El método de consumo take siempre se bloqueará y esperará.Para getTask(), siempre hay un valor de retorno, es decir, el hilo no se destruirá. Para lograr: cuando el número de subprocesos <= corePoolSize, los subprocesos inactivos no se reciclarán.

3. Ejecutor de tareas - Trabajador

En ThreadPoolExecutor, hay una clase interna de Worker. En pocas palabras, es el contenedor de tareas (implementación de ejecutable - tarea), que puede entenderse más o menos como un hilo, pero no exactamente lo mismo.

que es trabajador

De hecho, Worker es un contenedor de tareas y su método de construcción es muy simple:

    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);
        }
    }

Cree un nuevo objeto de subproceso y agréguele una tarea.

Cómo el Ejecutor administra al Trabajador

En primer lugar, Executor almacena Workers a través de un conjunto para garantizar la singularidad de cada trabajador.

	private final HashSet<Worker> workers = new HashSet<>();

En segundo lugar, para el grupo de subprocesos, lo más importante es addWorker y runWorker. No publicaré el código fuente aquí, pero hablaré brevemente sobre la diferencia y el uso de estas dos funciones.

La función addWorker() es donde realmente y se inicia el subproceso , porque la función thread.start() se encuentra en él. Después de una pequeña búsqueda, podemos ver que se llama a addWorker en la función de ejecución (), por lo que podemos saber que si el grupo de subprocesos quiere ejecutar tareas, debe llamar a la función de ejecución ().

La función runWorker() es en realidad la ejecución del Worker (explicación complementaria).

4. Función de ejecución - ejecutar

El código aquí revela la estrategia de procesamiento del grupo de subprocesos cuando el número de subprocesos es diferente.

inserte la descripción de la imagen aquí
La imagen de arriba está tomada de: https://thinkwon.blog.csdn.net/article/details/102541900

    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);
        }

		//阻塞队列满,新建线程执行任务知道 max,如果创建失败或任务超过max,则启用饱和策略——中断。
        else if (!addWorker(command, false))
            reject(command);
    }
5. Estrategia de saturación - controlador

La estrategia de saturación se usa para lidiar con la tarea actual cuando el subproceso es anormal y la cantidad de subprocesos está llena. Puedes entender un poco.

  • AbortPolicy: cancela la ejecución de la tarea enviada y lanza una excepción RejectedExecutionException; esta es la política predeterminada en el grupo de subprocesos.
    	//默认饱和策略
    	private static final RejectedExecutionHandler defaultHandler =
        	new AbortPolicy();

		//实现在池子中的内部类
	    public static class AbortPolicy implements RejectedExecutionHandler {
    
    

        public AbortPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  • CallerRunsPolicy: usa el subproceso de la persona que llama para ejecutar la tarea, es decir, desde la perspectiva de la persona que llama, esta tarea se serializa en el subproceso principal de la persona que llama.
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
    
    

        public CallerRunsPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                r.run();
            }
        }
    }
  • DiscardPolicy: descartar tareas directamente sin procesamiento (ejecutar funciones vacías);
    public static class DiscardPolicy implements RejectedExecutionHandler {
    
    
    
        public DiscardPolicy() {
    
     }
        
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        
        }
    }
  • DiscardOldestPolicy: descarta la tarea principal en la cola de bloqueo (es decir, la tarea con el tiempo de almacenamiento más largo) y ejecuta la tarea actual.
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    
    

        public DiscardOldestPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

Sugerencias:
De hecho, la estrategia de saturación de Java se puede considerar como una introducción. En escenarios comerciales, puede personalizar la estrategia de saturación para funciones comerciales específicas. Esa es la clave.

4. ¿Por qué usar el grupo de subprocesos?

Después de la comprensión anterior, podemos resumir el valor del grupo de subprocesos:

  • Reducir el consumo de recursos. Reduzca el consumo de recursos de creación y destrucción de subprocesos mediante la reutilización de subprocesos existentes;
  • Mejorar la capacidad de gestión de subprocesos. Los subprocesos son recursos escasos y no se pueden multiplicar indefinidamente. Demasiados subprocesos también sobrecargarán la computadora.

Supongo que te gusta

Origin blog.csdn.net/weixin_43742184/article/details/113736775
Recomendado
Clasificación