Principios de diseño del grupo de subprocesos de Android

Proceso: primero se crea un proceso antes de que se ejecute cada aplicación. Zygote bifurca este proceso y lo utiliza para alojar varios Actividad / Servicio y otros componentes que se ejecutan en la Aplicación. El proceso es completamente transparente para la aplicación de la capa superior, que también es la intención de Google de hacer que el programa de la aplicación se ejecute en Android Runtime. En la mayoría de los casos, una aplicación se ejecuta en un proceso a menos que el atributo Android: process esté configurado en AndroidManifest.xml, o el proceso se bifurca a través del código nativo.  

Tema: Hilo de aplicaciones es muy común, como cada nuevo hilo () inicia se crea un nuevo hilo. Uso compartido de recursos entre el subproceso y el proceso donde se encuentra la aplicación. Desde la perspectiva de Linux, no existe una diferencia esencial entre procesos y subprocesos, excepto si comparten recursos. Todos son una estructura de estructura de tareas. Desde la perspectiva de la CPU, el proceso o el subproceso no es más que un ejecutable En el código, la CPU utiliza el algoritmo de programación CFS para garantizar que cada tarea disfrute del intervalo de tiempo de la CPU lo más justo posible.


1. Acerca del grupo de subprocesos 

El concepto de grupo de subprocesos en Android proviene de Executor en Java, y su uso es básicamente el mismo. Executor es una interfaz, la implementación del grupo de subprocesos real es ThreadPoolExecutor. ThreadPoolExecutor proporciona una serie de parámetros para configurar el grupo de subprocesos. Varios grupos de subprocesos comúnmente utilizados en Android se implementan mediante diferentes configuraciones de ThreadPoolExecutor.

Constructor ThreadPoolExecutor

ThreadPoolExecutor tiene múltiples métodos sobrecargados, pero finalmente este constructor se llama:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                    BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)
复制代码

corePoolSize: el número de hilos centrales en el grupo de hilos. 

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

keepAliveTime: el tiempo de espera de subprocesos no centrales. Cuando el tiempo de inactividad de subprocesos no centrales en el sistema excede keepAliveTime, se reciclará. Si el atributo allowCoreThreadTimeOut de ThreadPoolExecutor se establece en verdadero, entonces este parámetro también indica la duración del tiempo de espera del subproceso central.  

unidad: la unidad del parámetro keepAliveTime, hay nanosegundos, microsegundos, milisegundos, segundos, minutos, horas, días, etc.

 workQueue: la cola de tareas en el grupo de subprocesos. Esta cola se utiliza principalmente para almacenar tareas que se han enviado pero aún no se han ejecutado. Las tareas almacenadas aquí se envían mediante el método de ejecución de ThreadPoolExecutor. 

threadFactory: proporciona la función de crear nuevos subprocesos para el grupo de subprocesos. Generalmente usamos el predeterminado.

 manejador: estrategia de rechazo, cuando un subproceso no puede realizar una nueva tarea (generalmente porque el número de subprocesos en el grupo de subprocesos ha alcanzado el número máximo o el grupo de subprocesos está cerrado), de forma predeterminada, cuando el grupo de subprocesos no puede procesar nuevos subprocesos, se lanzará Una excepción RejectedExecutionException. 

Dos métodos de ejecución

ThreadPoolExecutor tiene dos métodos para ejecutar, a saber, submit () y execute (), veamos primero la diferencia entre estos dos métodos.

El código fuente del método execute ():

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();
        }
        //判断线程池是否为RUNNING状态,并且将任务添加至队列中.
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //审核下线程池的状态,如果不是RUNNING状态,直接移除队列中
            if (! isRunning(recheck) && remove(command))
                reject(command);
                //如果当前线程数量为0,则单独创建线程,而不指定任务.
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果不满足上述条件,尝试创建一个非核心线程来执行任务,如果创建失败,调用reject()方法.
        else if (!addWorker(command, false))
            reject(command);
    }
复制代码

Código fuente del método Submit ():

public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        //还是通过调用execute
        execute(ftask);
        //最后会将包装好的Runable返回
        return ftask;
    }

//将Callable<T> 包装进FutureTask中
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

//可以看出FutureTask也是实现Runnable接口,因为RunableFuture本身就继承了Runnabel接口
public class FutureTask<V> implements RunnableFuture<V> {
    .......
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}
复制代码

A partir del código fuente de los dos métodos anteriores, podemos analizar varias conclusiones. De hecho, submit () todavía necesita llamar a execute () para ejecutar la tarea. La diferencia es que submit () devuelve la tarea empaquetada y devolverá un objeto Future. . Desde el método execute (), no es difícil ver que el método addWorker () es el método principal para crear subprocesos (subprocesos centrales, subprocesos no centrales), y el método de rechazo () crea devoluciones de llamada para fallas de subprocesos. Por lo tanto, en circunstancias normales, cuando no se requiere la ejecución de subprocesos para devolver el valor del resultado, utilizamos el método de ejecución. Y cuando necesitemos devolver el valor, luego usar el método de envío, él devolverá un objeto Futuro. El futuro no solo puede obtener un resultado, sino que también se puede cancelar, podemos cancelar la ejecución de un futuro llamando al método cancel () del futuro. Por ejemplo, hemos agregado un hilo, pero en el proceso que queremos interrumpirlo, se puede lograr a través de sumbit.

En segundo lugar, ¿las ventajas de usar el grupo de subprocesos?

 1. Evite la creación y destrucción de hilos frecuentes. Aunque el uso de Thread para crear un thread puede llevar a cabo operaciones que requieren mucho tiempo, la gran cantidad de threads creados y destruidos causará una sobrecarga de rendimiento excesiva.

 2. Evite la presión sobre los recursos del sistema. Cuando una gran cantidad de subprocesos trabajan juntos, puede causar tensión en los recursos. El mecanismo subyacente al subproceso también se divide en tiempo de CPU. Cuando existe una gran cantidad de subprocesos al mismo tiempo, pueden causar que los recursos se eviten entre sí, lo que resulta en un bloqueo. Fenómeno  

3. Mejor gestión de hilos . Tomando la función de descarga como un ejemplo, en circunstancias normales, habrá un límite en el número máximo de descargas simultáneas, y usando el grupo de subprocesos, podemos establecer de manera flexible el número máximo de descargas simultáneas, la ejecución en serie de secuencias de tareas de descarga y la espera en cola de acuerdo con las necesidades reales. 

Tres, varios grupos de subprocesos de uso común en Android

3.1 FixedThreadPool

Su código fuente es el siguiente:

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

Podemos ver dos características del código fuente: 

 1. Tiene solo un parámetro entrante, es decir, un número fijo de subprocesos centrales. Solo proporciona un nThreads para entrantes externos, y su número de subproceso central es el mismo que el número máximo de subprocesos. Esto muestra que no hay un subproceso no principal en FixedThreadPool, todos los subprocesos son subprocesos principales. 

 2. El tiempo de espera del hilo es 0. Esto muestra que el hilo central no se destruirá incluso cuando no haya ninguna tarea que realizar, lo que permite que FixedThreadPool responda a las solicitudes más rápidamente. La última cola de subprocesos es LinkedBlockingQueue, pero LinkedBlockingQueue no tiene parámetros, lo que muestra que el tamaño de la cola de subprocesos es Integer.MAX_VALUE (2 ^ 31-1) De los parámetros del código fuente anteriores, también deberíamos tener una comprensión general de las características de trabajo de FixedThreadPool, a continuación Continuamos analizando varios otros grupos de subprocesos. 

 3.2 SingleThreadExecutor 

Su código fuente es el siguiente:

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

Desde el código fuente, podemos encontrar fácilmente que SingleThreadExecutor es similar a FixedThreadPool. La diferencia es que el número de subprocesos principales de SingleThreadExecutor es solo 1, lo que significa que solo se puede ejecutar un subproceso al mismo tiempo. Úselo para evitar que lidiemos con problemas de sincronización de subprocesos. Para usar una analogía, es similar a hacer cola para comprar boletos, solo puede manejar los asuntos de una persona a la vez. De hecho, si pasamos el parámetro de FixedThreadPool como 1, es lo mismo que SingleThreadExecutor.

3.3 CachedThreadPool

Su código fuente es el siguiente:

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

Como puede ver en el código fuente,

No hay subproceso principal en CachedThreadPool, pero su número máximo de subprocesos es Integer.MAX_VALUE. Además, CachedThreadPool tiene un mecanismo de tiempo de espera de subprocesos y su período de tiempo de espera es de 60 segundos. Dado que el número máximo de subprocesos es infinito, cada vez que se agrega una nueva tarea, si hay un subproceso inactivo en el grupo de subprocesos, el subproceso inactivo ejecutará la nueva tarea; si no hay un subproceso inactivo, se creará un nuevo subproceso para ejecutar la tarea . De acuerdo con las características de CachedThreadPool, CachedThreadPool se puede usar cuando hay una gran cantidad de solicitudes de tareas que consumen mucho tiempo, porque cuando no hay una nueva tarea en CachedThreadPool, todos los subprocesos se terminarán debido al tiempo de espera de 60 segundos.

3.4 ScheduledThreadPool

Su código fuente es el siguiente:

public ScheduledThreadPoolExecutor(int corePoolSize) {  
    super(corePoolSize, Integer.MAX_VALUE,  
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,  
          new DelayedWorkQueue());  
} 
复制代码

Se puede ver en el código fuente que el número de hilos centrales es fijo, pero los hilos no básicos son infinitos. Cuando el subproceso no central está inactivo, se reciclará de inmediato.
ScheduledThreadPool también es el único entre los cuatro grupos de subprocesos que tiene la función de ejecutar tareas periódicamente. Es adecuado para realizar algunas tareas periódicas o tareas retrasadas.

Cuarto, ¿cómo terminar una tarea de subproceso en el grupo de subprocesos?

java.lang.ThreadLa clase contiene algunos métodos de uso común, como: start (), stop (), stop (Throwable), suspend (), destroy (), resume (). A través de estos métodos, podemos realizar operaciones convenientes en el hilo, pero entre estos métodos, solo start()se retiene el método.

《Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?》Las razones para abandonar estos métodos se explican   en la documentación de ayuda de JDK y en un artículo de Sun.

  La razón simple es que, aunque el método de detención se puede utilizar para 强行终止ejecutar o suspender subprocesos, es muy útil utilizar el método de detención 危险, al igual que apagar la computadora de forma repentina en lugar de apagarla de acuerdo con los procedimientos normales, puede producir resultados impredecibles. Por lo tanto, no se recomienda utilizar el método de detención para terminar el subproceso.

 Entonces, ¿cómo debemos detener el hilo?

  • 1. Por lo general, hay una estructura de bucle en la tarea, siempre que se use una marca para controlar el bucle, la tarea puede finalizar.
  • 2. Si el hilo está adentro 冻结状态y no puede leer la marca, puede usar un interrupt()método para traer el hilo 从冻结状态强制恢复到运行状态中para que el hilo califique para la ejecución de la CPU.

Después de que el hilo general ejecuta el método de ejecución, el hilo termina normalmente, por lo que hay varias formas de implementarlo:

4.1 Usando Future y Callable

Pasos:

  1. Implemente la interfaz invocable
  2. Llame al método pool.submit () para devolver el objeto Future
  3. Use el objeto Futuro para obtener el estado del hilo.

private void cancelAThread() {
        ExecutorService pool = Executors.newFixedThreadPool(2);
          
          Callable<String> callable = new Callable<String>() {
              
            @Override
            public String call() throws Exception {
                System.out.println("test");
                return "true";
            }
        };
          
        Future<String> f = pool.submit(callable);
          
        System.out.println(f.isCancelled());
        System.out.println(f.isDone());
        f.cancel(true);
  
    }
复制代码

【Curioso】

(1) ¿Cómo será la implementación interna de future.cancel (mayInterruptIfRunning)? Puede interrumpir la ejecución de "esa" tarea en un grupo de subprocesos.

Se puede adivinar que se debe registrar la identificación específica del hilo y se envía una señal de interrupción.

(2) Supongo que debería enviar una señal de interrupción para interrumpir la operación en el bloque. Si es while (verdadero); esta operación sin bloqueo que ocupa la CPU no se puede interrumpir, es decir, el hilo todavía se está ejecutando, ocupando los recursos del grupo de hilos.

【Nota】

a). Los recursos del grupo de subprocesos son limitados, algunas tareas no se enviarán (), arrojará una excepción: java.util.concurrent.RejectedExecutionException

b). Siempre que submit () tenga éxito, no importa si el hilo se está ejecutando o está esperando su ejecución en BlockingQueue, la operación future.cancel () puede interrumpir el hilo. Es decir, no tiene nada que ver con su ejecución real, y se interrumpirá mientras bloquea o espera a que se programe la ejecución.

4.2 Usando la palabra clave volátil

Establezca la bandera de salida para terminar el hilo.

public class ThreadSafe extends Thread {
    public volatile boolean isCancel = false; 
        public void run() { 
        while (!isCancel){
           //TODO method(); 
        }
    } 
}

复制代码

4.3 Usando el método interrupt ()

Termine el hilo y atrape la excepción.

El método Thread.interrupt () consiste en interrumpir el hilo. El bit de estado de interrupción del subproceso se establecerá, es decir, se establecerá en verdadero. El resultado de la interrupción es si el subproceso muere, si espera una nueva tarea o si continúa al siguiente paso, dependiendo del programa en sí. El hilo verificará este indicador de interrupción de vez en cuando para determinar si el hilo debe interrumpirse (si el valor del indicador de interrupción es verdadero). No interrumpe un hilo en ejecución como el método stop 

public class ThreadSafe extends Thread {
  
   @Override
    public void run() { 
        while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
            try{
              //TODO method(); 
              //阻塞过程捕获中断异常来退出
            }catch(InterruptedException e){
                e.printStackTrace();
                break;//捕获到异常之后,执行break跳出循环。
            }
        }
    } 
}复制代码

  El método interrupt () simplemente cambia el estado de interrupción y no interrumpe un hilo en ejecución. Los usuarios necesitan monitorear el estado del hilo y hacer el procesamiento ellos mismos. El método para soportar la interrupción del hilo (es decir, el método de lanzar una excepción interrumpida después de una interrupción del hilo) es monitorear el estado de interrupción del hilo. Una vez que el estado de interrupción del hilo se establece en "estado de interrupción", se lanzará una excepción de interrupción. Lo que este método realmente logra es enviar una señal de interrupción al hilo bloqueado para que el hilo bloqueado detecte el indicador de interrupción y pueda salir del estado bloqueado.

Más específicamente, si el subproceso está bloqueado por uno de los tres métodos Object.wait, Thread.join y Thread.sleep, y se llama al método interrupt () del subproceso en este momento, el subproceso generará una InterruptedException (el subproceso Debe estar preparado de antemano para hacer frente a esta excepción), a fin de terminar el estado bloqueado antes. Si el hilo no está bloqueado, llamar a interrupt () no tendrá ningún efecto en este momento, y la InterruptedException no se lanzará inmediatamente hasta la ejecución de wait (), sleep (), join ().

this.interrupted () : prueba si el hilo actual ha sido interrumpido (método estático). Si el método se llama continuamente, la segunda llamada devolverá falso. En la documentación de la API se indica que el método interrumpido () tiene la función de borrar el estado. Después de la ejecución, tiene la función de borrar el indicador de estado a falso.

this.isInterrupted (): prueba si el hilo ha sido interrumpido, pero el indicador de estado no se puede borrar.


Supongo que te gusta

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