Caja de subprocesos múltiples (3): temporizador, grupo de subprocesos

1. Temporizador

Función de temporizador: acuerde un intervalo de tiempo y, cuando llegue el momento, ejecute una determinada parte de la lógica del código. De hecho, es un "despertador".

1.1 Usar temporizadores en la biblioteca estándar

  • La biblioteca estándar proporciona una clase Timer, cuyo método principal es la programación.
  • La clase Timer contiene un hilo de escaneo para observar si alguna tarea ha alcanzado el tiempo de ejecución.
  • El programa contiene dos parámetros: el primer parámetro especifica el código de tarea que se ejecutará y el segundo parámetro especifica cuánto tiempo llevará ejecutarse después (unidad: milisegundos).
  • La clase TimerTask hereda la interfaz Runnable, por lo que puede anular el método run() 
public class Test {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        },1000);
    }
}

Esto se debe a que el hilo dentro del Temporizador impide el final del proceso. Se pueden organizar múltiples tareas en el Temporizador. Debemos prestar atención a esto cuando lo implementemos a continuación.

1.2 Implementación del temporizador

1. Se necesita un hilo de escaneo en Timer para escanear si la tarea ha caducado y si se puede ejecutar.

2. El temporizador puede programar la ejecución de múltiples tareas y el tiempo de ejecución de cada tarea es diferente, por lo que necesitamos usar una cola de prioridad para almacenar tareas y organizarlas en orden cronológico.

3. También necesita crear una clase para describir una tarea a través de la clase (incluido el contenido y el tiempo de la tarea)

class MyTimer{
    private PriorityQueue<MyTimerTask> priorityQueue = new PriorityQueue<>();

    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() -> {
            while(true){
                synchronized (this){//涉及到修改操作,加锁
                    try{
                        while(priorityQueue.isEmpty()){
                            this.wait();
                        }

                        MyTimerTask myTimerTask = priorityQueue.peek();
                        long curTime = System.currentTimeMillis();//得到当前时间

                        if(curTime >= myTimerTask.getTime()){//到达执行时间
                            myTimerTask.getRunnable().run();
                            priorityQueue.poll();
                        }else {//未到达执行时间
                            this.wait(myTimerTask.getTime() - curTime);//线程等待
                            //如果没有这句代码,就会出现忙等,类似于,一直在看表
                        }
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long delay){
        synchronized (this){
            MyTimerTask myTimerTask = new MyTimerTask(runnable,delay);
            priorityQueue.offer(myTimerTask);//将任务放入队列
            this.notify();//如果当前队列为空,唤醒线程
        }
    }
}

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;//任务内容
    private long time;//任务执行的具体时间

    public MyTimerTask(Runnable runnable, long delay){
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    //得到任务执行的时间
    public long getTime(){
        return time;
    }

    //得到任务内容
    public Runnable getRunnable() {
        return runnable;
    }

    //重写比较方法,按照时间顺序从小到大排列
    @Override
    public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);
    }
}

Hay algunos detalles a tener en cuenta sobre el código anterior:

1. 

Debido a que wait () también puede ser interrumpido por InterruptedException, si usa if, la cola todavía es nula en este momento y no puede ocurrir ningún error.

2.

Porque si usas el modo de suspensión, hay un escenario que no se puede establecer, es decir, cuando insertamos una tarea con un tiempo de ejecución anterior, el hilo todavía está en estado de suspensión, en este momento la tarea recién insertada se retrasará en su ejecución. , lo que no se ajusta a nuestra lógica.

Si usa esperar, el hilo encontrará la primera tarea para ejecutarse nuevamente.

2. Grupo de subprocesos

El grupo de subprocesos puede reducir la sobrecarga de creación y destrucción de subprocesos, lo que significa que es adecuado para escenarios en los que los subprocesos se crean y destruyen con frecuencia.

2.1 Usar el grupo de subprocesos en la biblioteca estándar

Varias formas para que los ejecutores creen grupos de subprocesos

  • newFixedThreadPool: crea un grupo de subprocesos con un número fijo de subprocesos
  • newCachedThreadPool: cree un grupo de subprocesos con un número de subprocesos que crece dinámicamente. Una vez ejecutado el subproceso, no se destruirá inmediatamente, sino que se almacenará en caché durante un período de tiempo.
  • newSingleThreadExecutor: crea un grupo de subprocesos que contiene solo un subproceso.
  • newScheduledThreadPool: ejecuta el comando después de configurar el tiempo de retraso, o ejecuta el comando regularmente, es una versión avanzada de Timer.
public class Demo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        ExecutorService service1 = Executors.newFixedThreadPool(3);
        ExecutorService service2 = Executors.newSingleThreadExecutor();
        ExecutorService service3 = Executors.newScheduledThreadPool(2);

        service.submit(new Runnable() {//通过 ExecutorService.submit 可以注册一个任务到线程池
            @Override
            public void run() {
                System.out.println("111");
            }
        });
    }
}

¿Por qué no utilizar el nuevo método aquí para crear un grupo de subprocesos, pero utilizar el "modo de fábrica" ​​para implementarlo?

Primero, comprendamos qué es el patrón de fábrica. El patrón de fábrica se refiere al uso de métodos ordinarios en lugar de constructores para completar el trabajo de inicialización. Debido a que los métodos ordinarios se pueden distinguir por los nombres de los métodos, no están sujetos a reglas de sobrecarga. Por ejemplo: nuestras coordenadas pueden usar el sistema de coordenadas cartesiano o el sistema de coordenadas polares. Los parámetros de estos dos métodos de construcción son exactamente los mismos. En este momento, necesitamos usar el "modo de fábrica" ​​para inicializar.

Los ejecutores son esencialmente una encapsulación de la clase ThreadPoolExecutor. ThreadPoolExecutor proporciona más parámetros opcionales, que pueden refinar aún más la configuración del comportamiento del grupo de subprocesos. Introduzcamos los parámetros del método de construcción (¡¡¡muy importante !!!).

  • corePoolSize: el número mínimo de subprocesos en el grupo de subprocesos
  • MaximumPoolSize: el número máximo de subprocesos en el grupo de subprocesos.
  • keepAliverTime: ¿Cuánto dura el tiempo de "pesca" del hilo? Si un hilo no ha funcionado durante los tiempos de keepAliverTime, el hilo será destruido.
  • unidad: unidad de keepAliverTime
  • workQueue: cola de bloqueo. Si se requiere prioridad, configure PriorityBlockingQueue. Si hay un límite de cantidad, configure ArrayBlockingQueue. Si el número cambia mucho, configure LinkedBlockingQueue.
  • threadFactory: modo de fábrica, use el modo de fábrica para crear subprocesos y establecer algunas propiedades del subproceso
  • controlador: la política de rechazo del grupo de subprocesos. Hay un límite superior en la cantidad de tareas que un grupo de subprocesos puede acomodar. Cuando se alcanza el límite superior, el método de procesamiento para agregar subprocesos continúa. Los cuatro métodos de procesamiento son los siguientes:

 Aquí hay otra pregunta clásica de la entrevista: si necesita establecer la cantidad de subprocesos para usar un grupo de subprocesos, ¿cuál es la configuración adecuada?

En este momento, siempre que responda el número específico, será incorrecto, porque hay dos tipos de código ejecutado por un hilo:

1) Uso intensivo de CPU: el código realiza principalmente operaciones aritméticas/lógicas

2) IO intensivo: el código realiza principalmente operaciones IO

Supongamos que todo el código de un subproceso consume mucha CPU. En este momento, el número de subprocesos en el grupo de subprocesos no debe exceder N (número de núcleos lógicos de la CPU). Si es mayor que N, no se puede mejorar la eficiencia.

Suponiendo que todo el código de un subproceso consume mucha IO y no consume CPU en este momento, puede exceder N en este momento.

Dependiendo del código, la cantidad de subprocesos en un grupo de subprocesos se establece de manera diferente, porque no podemos saber cuánto de un fragmento de código consume mucha CPU y cuánto consume IO. La respuesta correcta es: utilice métodos experimentales para realizar pruebas de rendimiento en el programa y ajuste la cantidad de subprocesos en el grupo de subprocesos de vez en cuando durante la prueba para ver qué situación cumple mejor con los requisitos.

 2.2 Implementación simple del grupo de subprocesos

import java.util.concurrent.*;

class MyThreadPool{
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(4);

    public void submit(Runnable runnable){
        try {
            blockingQueue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                try {
                    Runnable a = blockingQueue.take();
                    a.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}

Supongo que te gusta

Origin blog.csdn.net/m0_74859835/article/details/132918361
Recomendado
Clasificación