Consolide la base de subprocesos múltiples en 10 minutos

Consolide la base de subprocesos múltiples en 10 minutos

Prefacio

El subproceso múltiple es la base de la programación concurrente, este artículo hablará sobre el subproceso múltiple.

Primero hablemos de conceptos, como procesos y subprocesos, serialidad, paralelismo y concurrencia.

Hablemos del estado del hilo, prioridad, sincronización, comunicación, terminación y otros conocimientos.

Procesos e hilos

¿Qué es un proceso?

El sistema operativo asigna recursos a los procesos y utiliza procesos para la programación, pero cuando un proceso encuentra una tarea de bloqueo, cambiará de proceso para mejorar la utilización de la CPU.

Debido a que el costo de cambiar de proceso es demasiado alto, nacieron los hilos

Los subprocesos también se denominan procesos ligeros (LWP). Los subprocesos son la unidad de programación básica del sistema operativo. Cuando los subprocesos se asignan a intervalos de tiempo determinados por la CPU, pueden programar tareas.

Cuando un subproceso se bloquea mientras espera un recurso, el subproceso se suspenderá para mejorar la utilización de la CPU. Cuando los recursos posteriores estén listos, el subproceso se reanudará y la ejecución continuará después de que se le asigne un intervalo de tiempo.

Por motivos de seguridad, los subprocesos se dividen en modo de usuario y modo kernel. Cuando utilice subprocesos para realizar tareas ordinarias, puede programar la ejecución en modo de usuario. Cuando desee completar ciertas operaciones relacionadas con la seguridad del sistema operativo, debe cambiar a modo kernel primero antes de continuar.

La suspensión y reanudación de subprocesos requiere cambiar entre el modo de usuario y el modo kernel, y el cambio frecuente de subprocesos también generará cierta sobrecarga.

Cuando hacemos clic para abrir el navegador, el programa del navegador puede iniciar uno o más procesos.

Hay uno o más subprocesos en un proceso. El proceso se utiliza para administrar los recursos asignados por el sistema operativo. El subproceso se utiliza para la programación. Todos los subprocesos del mismo proceso pueden compartir los recursos del proceso. Para almacenar el estado de ejecución de la tarea programada, el hilo también tendrá su propio espacio de memoria privado para almacenarlo.

Hay tres tipos de implementaciones de modelos de subprocesos en modo usuario y modo kernel: uno a uno entre subprocesos de usuario y subprocesos del kernel, muchos a uno y muchos a muchos.

modelo uno a uno

El modelo uno a uno es sencillo de implementar. Un subproceso de usuario se asigna a un subproceso del núcleo. El modelo utilizado en Java es uno a uno.

imagen.png

Sin embargo, si los subprocesos se utilizan incorrectamente, puede provocar cambios frecuentes de estados del kernel, lo que genera una gran sobrecarga.

Y los recursos de subprocesos del núcleo son limitados , por lo que existe un límite superior para los recursos de subprocesos en el modelo uno a uno.

muchos a uno

En modelo muchos a uno

imagen.png

Dado que varios subprocesos de usuario asignan el mismo subproceso del núcleo, se pueden utilizar más subprocesos de usuario en comparación con el modelo uno a uno.

Sin embargo, cuando se produce el bloqueo, es necesario cambiar al estado del kernel para bloquear. Se bloquearán todos los subprocesos de usuario correspondientes al subproceso del kernel y su implementación también se volverá complicada.

muchos a muchos

En un modelo de muchos a muchos

imagen.png

No solo resuelve el problema del límite superior de subprocesos del modelo uno a uno, sino que también resuelve el problema del bloqueo de subprocesos del núcleo correspondiente al bloqueo de todos los subprocesos del usuario en el modelo muchos a uno.

Pero la implementación se vuelve más compleja.

Serie, Paralelo y Concurrente

¿Por qué utilizar subprocesos múltiples?

Con el desarrollo del hardware, la mayoría de las máquinas ya no son máquinas con CPU de un solo núcleo, sino que una gran cantidad de máquinas utilizan tecnología de hiperprocesamiento de múltiples núcleos.

La serialización puede entenderse como la ejecución en cola. Cuando al subproceso se le asignan recursos de CPU, comienza la programación. El subproceso puede programar tareas de E/S.

En este momento, esperará a que los recursos de IO estén listos antes de programar. Durante este período, la CPU no hace nada y la CPU no se utiliza de manera efectiva.

imagen.png

Para mejorar la utilización de la CPU, cuando el subproceso A está esperando recursos de IO, el subproceso A se puede suspender primero y asignar recursos de CPU al subproceso B.

Cuando el recurso IO que el subproceso A está esperando está listo, el subproceso B se suspende y el subproceso A se reanuda para continuar la ejecución.

Los dos subprocesos parecen estar ejecutándose al mismo tiempo durante un período de tiempo, de hecho, se ejecutan alternativamente, solo un subproceso se ejecuta en un momento determinado.

La concurrencia mejora la utilización de la CPU, pero también genera la sobrecarga del cambio de contexto de subprocesos.

imagen.png

Entonces ¿qué es el paralelismo?

La serialización y concurrencia mencionadas anteriormente se pueden lograr en un solo subproceso, pero el requisito previo para el paralelismo es multinúcleo.

El paralelismo significa que se ejecutan varios subprocesos al mismo tiempo en un momento determinado, por lo que se requieren varios núcleos.

imagen.png

Entonces, ¿el subproceso múltiple es necesariamente el más rápido?

Después del análisis anterior, sabemos que: suspensión y recuperación de subprocesos, el cambio de contexto pasará por la conversión del modo de usuario y el modo kernel, y habrá una sobrecarga de rendimiento.

Cuando hay demasiados subprocesos y se producen frecuentes cambios de contexto durante el tiempo de ejecución, la sobrecarga de rendimiento puede incluso superar los beneficios de la mejora de la concurrencia en la utilización de la CPU.

Crear hilo

La clase de subproceso que se nos proporciona en JDK es java.lang.Threadque implementa la interfaz Runnable y utiliza una construcción para aceptar la implementación de Runnable.

  public class Thread implements Runnable {
      private Runnable target;
  }

La interfaz Runnable es una interfaz funcional, que solo tiene un método de ejecución. La implementación en el método de ejecución indica las tareas que se realizarán después de que se inicia el hilo.

  public interface Runnable {
      public abstract void run();
  }

Solo hay una forma de crear un hilo en Java: crear un objeto Thread y luego llamar al método de inicio para iniciar el hilo.

Podemos crear un hilo a través del constructor y establecer el nombre del hilo al mismo tiempo, y configurar la tarea a implementar (imprimir el nombre del hilo + hola)

      public void test(){
          Thread a = new Thread(() -> {
              //线程A hello
              System.out.println(Thread.currentThread().getName() + " hello");
          }, "线程A");
          //main hello
          a.run();
          a.start();
      }

Cuando se llama al método de ejecución en el hilo principal, en realidad es el hilo principal el que realiza las tareas de la interfaz ejecutable.

Como dijimos antes, el modelo de subprocesos en Java es un modelo uno a uno y un subproceso corresponde a un subproceso del núcleo.

Solo cuando se llama al método de inicio, se llama al método local (método C ++) para iniciar el hilo para ejecutar la tarea.

imagen.png

Si se llama a start dos veces, se generará IllegalThreadStateExceptionuna excepción .

Estado del hilo

El estado de Thread en Java se divide en nuevo, en ejecución, bloqueado, en espera, tiempo de espera de espera y terminación.

 public enum State {
     //新建
     NEW,
     //运行
     RUNNABLE,
     //阻塞
     BLOCKED,
     //等待
     WAITING,
     //超时等待
     TIMED_WAITING,
     //终止
     TERMINATED;
 }

En el sistema operativo, la ejecución se divide en estados listo y en ejecución. Después de crear el subproceso, el estado que espera a que la CPU asigne un intervalo de tiempo es el estado listo y el estado asignado al intervalo de tiempo es el estado de ejecución.

imagen.png

Nuevo: el subproceso acaba de crearse y aún no ha obtenido el intervalo de tiempo asignado por la CPU.

Ejecutar: el subproceso obtiene el intervalo de tiempo asignado por la CPU y realiza la programación de tareas

Bloqueo: durante el proceso de programación del hilo, entra en un estado bloqueado debido a la imposibilidad de obtener recursos compartidos (como ser bloqueado por sincronizado)

Esperando: durante el proceso de programación de subprocesos, ejecute esperar, unirse y otros métodos para ingresar al estado de espera y esperar a que se activen otros subprocesos.

Tiempo de espera de espera: durante el proceso de programación de subprocesos, se ingresa al estado de tiempo de espera de espera al ejecutar dormir (1), esperar (1), unirse (1) y otros métodos para establecer el tiempo de espera.

Terminación: el hilo completa la tarea programada o se ejecuta de manera anormal y entra en el estado de terminación.

prioridad

El requisito previo para que un subproceso programe tareas es obtener recursos de CPU (intervalo de tiempo asignado por la CPU)

Proporciona métodos en Java setPrioritypara establecer la prioridad para obtener recursos de la CPU. El rango es 1 ~ 10 y el valor predeterminado es 5.

  //最小
  public final static int MIN_PRIORITY = 1;
 
  //默认
  public final static int NORM_PRIORITY = 5;
  
  //最大
  public final static int MAX_PRIORITY = 10;

Pero la prioridad establecida es solo en el nivel de Java y la prioridad asignada al sistema operativo es diferente.

Por ejemplo, establecer la prioridad 5 o 6 en Java puede asignarse a la prioridad del sistema operativo en el mismo nivel.

hilo demonio

¿Qué es un hilo de demonio?

Los subprocesos de demonio pueden entenderse como subprocesos en segundo plano: cuando todos los subprocesos que no son de demonio en el programa hayan completado sus tareas, el programa finalizará.

En resumen, independientemente de si el subproceso del demonio ha terminado de ejecutarse, siempre que el subproceso que no sea del demonio haya terminado de ejecutarse, el programa finalizará.

Por lo tanto, el hilo del demonio se puede utilizar para realizar algunas operaciones en segundo plano para verificar recursos.

Cómo utilizar setDaemon(true)para convertir un hilo en un hilo de demonio

Sincronización de hilos

Cuando varios subprocesos necesitan utilizar recursos compartidos, no pueden obtenerlos al mismo tiempo debido a la cantidad limitada de recursos compartidos.

Solo un subproceso puede obtener el recurso compartido a la vez y otros subprocesos que no han obtenido el recurso compartido deben bloquearse.

Pueden producirse errores lógicos si varios subprocesos utilizan recursos compartidos al mismo tiempo.

La palabra clave sincronizada se usa comúnmente en Java para usar el bloqueo para garantizar la sincronización (solo un subproceso puede acceder a recursos compartidos)

         synchronized (object){
             System.out.println(object);
         }

donde el objeto es un recurso compartido bloqueado

Para obtener más descripciones de sincronizado, puede consultar este artículo: 15.000 palabras, 6 casos de código y 5 esquemas para ayudarle a comprender a fondo Sincronizado.

comunicación de hilo

Esperar/notificar notificar

Cuando se utiliza sincronizado, es necesario adquirir el bloqueo. Sólo después de adquirir el bloqueo el subproceso puede ejecutar la programación. Cuando las condiciones de ejecución no se cumplen en la programación, es necesario abandonar el bloqueo para permitir que se ejecuten otros subprocesos.

Por ejemplo, en el modelo productor/consumidor, cuando el productor obtiene el bloqueo para producir recursos y descubre que los recursos están llenos, debe abandonar el bloqueo y activarlo cuando el consumidor haya terminado de consumir.

Este modo de espera/notificación es una forma de implementar la comunicación de subprocesos. Java proporciona métodos de espera y notificación para implementar el modo de espera/notificación.

El requisito previo para usar esperar y notificar es obtener el bloqueo.

esperar permite que el hilo actual libere el bloqueo y entre en modo de espera, esperando a que otros hilos se activen usando notificar

esperar (1) también puede llevar el tiempo de espera ms. Cuando llegue el momento, se despertará automáticamente y comenzará a competir por el bloqueo.

notificar despierta un hilo esperando el bloqueo actual

notifyAll despierta todos los hilos que esperan el bloqueo actual

Para su implementación específica, puede ver 15,000 palabras, 6 casos de código y 5 diagramas esquemáticos para comprender completamente la sección de bloqueo pesado de la actualización de bloqueo de Synchronized.

modelo productor consumidor

La espera y la notificación se utilizan comúnmente en modelos de productores y consumidores para la comunicación de subprocesos.

Cuando el productor verifica que los recursos producidos están llenos, entra en espera, espera a que el consumidor se despierte después del consumo y luego lo despierta una vez completada la producción.

Cuando el consumidor comprueba que no hay recursos, entra en un estado de espera, espera a que el productor se despierte después de la producción y luego lo despierta después del consumo.

Producción

public void produce(int num) throws InterruptedException {
    synchronized (LOCK) {
        //如果生产 资源 已满 等待消费者消费
        while (queue.size() == 10) {
            System.out.println("队列满了,生产者等待");
            LOCK.wait();
        }
        
        Message message = new Message(num);
        System.out.println(Thread.currentThread().getName() + "生产了" + message);
        queue.add(message);
        //唤醒 所有线程
        LOCK.notifyAll();
    }
}

Consumo

public void consume() throws InterruptedException {
    synchronized (LOCK) {
        //如果队列为空 等待生产者生产
        while (queue.isEmpty()) {
            System.out.println("队列空了,消费者等待");
            LOCK.wait();
        }
        Message message = queue.poll();
        System.out.println(Thread.currentThread().getName() + "消费了" + message);
        //唤醒 所有线程
        LOCK.notifyAll();
    }
}

dormir dormir

El método de suspensión se utiliza para dejar que el hilo duerma durante un período de tiempo ms

La diferencia con esperar es que dormir no libera el bloqueo cuando duerme y no es necesario adquirir el bloqueo primero cuando usa dormir.

unirse esperar

El método de unión se utiliza para esperar a que un hilo termine de ejecutarse.

Por ejemplo, si se llama en el hilo principal, thread.join()debe esperar a que el hilo termine de ejecutarse antes de que regrese el método de unión.

Al mismo tiempo, unirse también admite la configuración del tiempo de espera ms y regresa automáticamente después del tiempo de espera.

Terminar hilo

Para terminar un hilo, generalmente use un método de terminación seguro: interrumpir el hilo

Cuando el hilo se está ejecutando, se guardará un bit de bandera, que por defecto es falso, lo que indica que ningún otro hilo lo interrumpirá.

Cuando desee que un hilo se detenga, puede interrumpirlo, por ejemplo 线程A.interrupt(): realice una operación de interrupción en el hilo A. En este momento, el indicador de interrupción del hilo A es verdadero.

Cuando un hilo programa una tarea, dejará de sondear cuando el indicador de interrupción sea verdadero. Puede usar 线程A.isInterrupted(): Ver el indicador de interrupción del hilo A.

Cuando un subproceso entra en estado de espera y es interrumpido por otros subprocesos, se producirá una excepción de interrupción. El bit de bandera se borrará y se generará una excepción de interrupción, que se puede capturar en el bloque catch para limpiar recursos o liberar recursos.

Al ejecutar en un bucle de acuerdo con el identificador de interrupción, también puede interrumpirse para detener y continuar la ejecución.

         Thread thread = new Thread(() -> {
             //中断标识为false就循环执行任务
             while (!Thread.currentThread().isInterrupted()) {
                 try {
                     //执行任务
                     System.out.println(" ");
                     
                     //假设等待资源
                     TimeUnit.SECONDS.sleep(1);
                     
                     //获得资源后执行
                     
                 } catch (InterruptedException e) {
                     //等待时中断线程会在抛出异常前恢复标志位
                     //捕获异常时,重新中断标志(自己中断)
                     Thread.currentThread().interrupt();
                     
                     //结束前处理其他资源
                 }
             }
             // true
             System.out.println(" 中断标识位:" + Thread.currentThread().isInterrupted());
         });

Hay otra forma de detectar interrupciones Thread.interrupted(): verifique la marca de interrupción del hilo actual, borre la marca de interrupción del hilo actual y devuelva la marca de interrupción a falso.

Finalmente (no lo hagas gratis, solo presiona tres veces seguidas para pedir ayuda~)

Este artículo se incluye en la columna " De punto a línea y de línea a superficie" para construir un sistema de conocimiento de programación concurrente Java en términos simples . Los estudiantes interesados ​​​​pueden seguir prestando atención.

Las notas y los casos de este artículo se han incluido en gitee-StudyJava y github-StudyJava . Los estudiantes interesados ​​pueden continuar prestando atención en stat ~

Dirección del caso:

Gitee-JavaConcurrentProgramming/src/main/java/A_Thread

Github-JavaConcurrentProgramming/src/main/java/A_Thread

Si tiene alguna pregunta, puede discutirla en el área de comentarios. Si cree que la escritura de Cai Cai es buena, puede darle me gusta, seguirla y recopilarla para respaldarla ~

Siga a Cai Cai y comparta más información útil, cuenta pública: la cocina privada back-end de Cai Cai

¡Este artículo es publicado por OpenWrite, un blog que publica varios artículos !

Lei Jun: La versión oficial del nuevo sistema operativo de Xiaomi, ThePaper OS, ha sido empaquetada. Una ventana emergente en la página de lotería de la aplicación Gome insulta a su fundador. El gobierno de Estados Unidos restringe la exportación de la GPU NVIDIA H800 a China. La interfaz de Xiaomi ThePaper OS está expuesto. Un maestro usó Scratch para frotar el simulador RISC-V y se ejecutó con éxito. Kernel de Linux Escritorio remoto RustDesk 1.2.3 lanzado, soporte mejorado para Wayland Después de desconectar el receptor USB de Logitech, el kernel de Linux falló Revisión aguda de DHH de "herramientas de empaquetado ": no es necesario construir la interfaz en absoluto (Sin compilación) JetBrains lanza Writerside para crear documentación técnica Herramientas para Node.js 21 lanzadas oficialmente
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10114995
Recomendado
Clasificación