Un artículo para comprender los conceptos básicos de subprocesamiento múltiple de Java y el modelo de memoria de Java


Escrito en frente: la mayoría de los estudiantes que mencionaron el subprocesamiento múltiple pueden fruncir el ceño, sin saber qué es el subprocesamiento múltiple, cuándo se puede usar, si hay problemas de variables compartidas al usarlo, etc. Este artículo se dividirá en dos partes: la primera parte explicará los conceptos básicos del subprocesamiento múltiple y la segunda parte explicará el modelo de memoria Java.

Inserte la descripción de la imagen aquí

1. Ciclo de vida multihilo y cinco estados básicos

Ciclo de vida de subprocesos múltiples de Java, primero mire el siguiente diagrama clásico, que básicamente contiene el conocimiento importante de subprocesos múltiples en Java.
Inserte la descripción de la imagen aquí
El hilo de Java tiene cinco estados básicos

  • Nuevo estado (Nuevo): cuando se crea el par de objetos de hilo, ingresa al nuevo estado, como: Hilo t = new MyThread ();

  • Estado listo (Ejecutable): cuando el método start () del objeto de subproceso se llama (t.start ();), el subproceso entra en el estado listo. El subproceso en el estado listo solo significa que el subproceso está listo para esperar a que la CPU programe la ejecución en cualquier momento, no que el subproceso se ejecutará inmediatamente después de ejecutar t.start ();

  • Estado de ejecución (En ejecución): cuando la CPU comienza a programar el subproceso en el estado preparado, el subproceso puede ejecutarse realmente en este momento, es decir, ingresar al estado de ejecución. Nota: El estado listo es la única entrada al estado de ejecución, es decir, si el hilo quiere ingresar al estado de ejecución para la ejecución, primero debe estar en el estado de listo;

  • Bloqueado (Bloqueado): el subproceso en el estado de ejecución abandona temporalmente el derecho a usar la CPU por algún motivo y detiene la ejecución. En este momento, ingresa en el estado bloqueado hasta que ingresa en el estado listo, y luego tiene la oportunidad de ser llamado por la CPU nuevamente para ingresar Al estado de ejecución. Según diferentes razones para el bloqueo, el estado de bloqueo se puede dividir en tres tipos:

1. Esperando bloqueo: el hilo en el estado de ejecución ejecuta el método wait () para hacer que el hilo entre en el estado de bloqueo en espera;

2. Bloqueo sincrónico: el hilo no puede adquirir el bloqueo sincronizado (porque el bloqueo está ocupado por otros hilos), ingresará al estado de bloqueo sincronizado;

3. Otro bloqueo: al llamar al hilo de suspensión () o unirse () o al emitir una solicitud de E / S, el hilo ingresará al estado de bloqueo. Cuando se agota el tiempo de espera (), join () espera a que el subproceso termine o agote el tiempo de espera, o el proceso de E / S se completa, el subproceso vuelve a entrar en el estado listo.

Dead (Dead): el subproceso finaliza la ejecución o sale del método run () debido a una excepción, y el subproceso finaliza su ciclo de vida.

En segundo lugar, la creación y el inicio de subprocesos múltiples de Java

Hay tres formas básicas de creación de hilos en Java

1. Herede la clase Thread y reescriba el método run () de esta clase

Herede la clase Thread y reescriba el método run () de esta clase

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0 ;i < 50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0;i<50;i++) {
            //调用Thread类的currentThread()方法获取当前线程
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 10) {
                new MyThread().start();
                new MyThread().start(); 
            }
        }
    }
}

Resultado de la operación:

...
main 48
main 49
Thread-00
Thread-01
Thread-02
Thread-03
Thread-04
Thread-10
...

Este es el resultado después de que se ejecuta el código, como se puede ver en la figura:

1. Hay tres hilos: principal, Hilo-0, Hilo-1

2. El valor de la variable miembro i salida por los dos hilos Thread-0 y Thread-1 no es continuo (donde i es una variable de instancia en lugar de una variable local). Porque: cuando se implementa el subproceso múltiple heredando la clase Thread, la creación de cada subproceso debe crear diferentes objetos de subclase, lo que da como resultado que los dos subprocesos Thread-0 y Thread-1 no puedan compartir la variable miembro i;

3. La ejecución del hilo es preventiva, y no dice que Thread-0 o Thread-1 siempre haya ocupado la CPU (esto también está relacionado con la prioridad del hilo. Aquí, la prioridad del hilo de Thread-0 y Thread-1 es la misma. No se expanda aquí)

2. Cree una clase de subproceso implementando la interfaz Runnable

Defina una clase para implementar la interfaz Runnable; cree un objeto de instancia obj de esta clase; pase obj como parámetro constructor en el objeto de instancia de clase Thread, este objeto es el objeto de hilo real

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0 ;i < 50 ;i++) {
            System.out.println(Thread.currentThread().getName()+":" +i);
        }
    }

    public static void main(String[] args) {
       for (int i = 0;i < 50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" +i);
            if (i == 10) {
                MyRunnable myRunnable = new MyRunnable();
                new Thread(myRunnable).start();
                new Thread(myRunnable).start();
            }
        }
        //java8 labdam方式
         new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        },"线程3").start();
    }
}

Resultado de la operación:

...
main:46
main:47
main:48
main:49
Thread-028
Thread-029
Thread-030
Thread-130
...

1. La variable miembro i que emite el subproceso 1 y el subproceso 2 es continua, lo que significa que la creación de un subproceso de esta manera puede permitir que varios subprocesos compartan la variable de instancia de la clase de subproceso, porque aquí múltiples subprocesos usan la misma instancia de destino Variable Sin embargo, cuando ejecuta utilizando el código anterior, encontrará que los resultados en realidad no son continuos. Esto se debe a que cuando varios subprocesos acceden al mismo recurso, si el recurso no está bloqueado, habrá problemas de seguridad de subprocesos (esto es Conocimiento de sincronización de hilos, no expandido aquí);
2, java8 puede usar lambda para crear múltiples hilos.

3. Crear un hilo a través de las interfaces invocables y futuras

Cree una clase de implementación de interfaz invocable e implemente el método call (), que actuará como el cuerpo de ejecución del subproceso, y el método tiene un valor de retorno, y luego cree una instancia de la clase de implementación invocable; use la clase FutureTask para envolver el objeto invocable, el objeto FutureTask encapsula el El valor de retorno del método call () del objeto invocable; use el objeto FutureTask como el objetivo del objeto Thread para crear e iniciar un nuevo hilo; llame al método get () del objeto FutureTask para obtener el valor de retorno después de la ejecución del hilo hijo

public class MyCallable implements Callable<Integer> {
    private int i = 0;
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建MyCallable对象
        Callable<Integer> myCallable = new MyCallable();
        //使用FutureTask来包装MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
        for (int i = 0;i<50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            if (i == 30) {
                Thread thread = new Thread(ft);
                thread.start();
            }
        }
        System.out.println("主线程for循环执行完毕..");
        Integer integer = ft.get();
        System.out.println("sum = "+ integer);
    }
}

El tipo de valor de retorno del método call () es el mismo que el tipo en <> cuando se crea el objeto FutureTask.

Tres, concepto de modelo de memoria Java

En la programación concurrente, debemos tratar con dos problemas clave: cómo comunicarse entre hilos y cómo sincronizarlos (los hilos aquí se refieren a la ejecución simultánea de entidades activas). La comunicación se refiere al mecanismo por el cual los hilos intercambian información. En la programación imperativa, hay dos mecanismos de comunicación entre hilos: memoria compartida y paso de mensajes.

En el modelo concurrente de memoria compartida, los hilos comparten el estado común del programa, y ​​los hilos se comunican implícitamente escribiendo-leyendo el estado común en la memoria. En el modelo de simultaneidad del paso de mensajes, no hay un estado común entre los hilos, y los hilos deben comunicarse explícitamente enviando mensajes explícitamente.

La memoria de almacenamiento dinámico se comparte entre subprocesos (este artículo utiliza el término "variables compartidas" para referirse a dominios de instancia, dominios estáticos y elementos de matriz). Las variables locales, los parámetros de definición del método (llamados parámetros formales del método en la especificación del lenguaje Java) y los parámetros del controlador de excepciones no se compartirán entre los subprocesos, no tendrán problemas de visibilidad de memoria, ni Influenciado por el modelo de memoria.

Interpretación de la memoria principal y la memoria de trabajo.

Memoria principal: el área donde existen instancias de una clase. Todas las instancias existen en la memoria principal, y el campo de la instancia también se encuentra aquí. Todos los subprocesos comparten la memoria principal, y la memoria principal corresponde principalmente a la porción de datos de instancia de los objetos en el montón de Java.

Memoria de trabajo: cada subproceso tiene su propia área de trabajo. En la memoria de trabajo, hay una copia parcial de la memoria principal, llamada copia de trabajo.

La comunicación entre subprocesos Java está controlada por el modelo de memoria Java (denominado JMM en este artículo). JMM determina cuándo la escritura de un subproceso de variables compartidas es visible para otro subproceso. Desde una perspectiva abstracta, JMM define la relación abstracta entre hilos y memoria principal: las variables compartidas entre hilos se almacenan en la memoria principal, y cada hilo tiene una memoria local privada (memoria local) , La memoria local almacena el hilo para leer / escribir una copia de la variable compartida. La memoria local es un concepto abstracto de JMM y realmente no existe. Cubre el almacenamiento en caché, las memorias intermedias de escritura, los registros y otras optimizaciones de hardware y compilador. El diagrama esquemático abstracto del modelo de memoria Java es el siguiente:

Inserte la descripción de la imagen aquí
De la figura anterior, si desea comunicarse entre el hilo A y el hilo B, debe seguir los dos pasos siguientes:

  1. Primero, el hilo A actualiza las variables compartidas actualizadas en la memoria local A a la memoria principal.
  2. Luego, el hilo B va a la memoria principal para leer la variable compartida que el hilo A ha actualizado antes.

A continuación se ilustran los dos pasos a través de un diagrama esquemático:
Inserte la descripción de la imagen aquí
como se muestra en la figura anterior, la memoria local A y B tienen una copia de la variable compartida x en la memoria principal. Suponga que inicialmente, los valores de x en las tres memorias son 0.
1. Durante la ejecución del subproceso A, el valor x actualizado (suponiendo un valor de 1) se almacena temporalmente en su memoria local A. Cuando el hilo A y el hilo B necesitan comunicarse, el hilo A primero actualizará el valor x modificado en su memoria local a la memoria principal, momento en el cual el valor x en la memoria principal se convierte en 1.
2. El subproceso B va a la memoria principal para leer el valor x actualizado del subproceso A. En este momento, el valor x de la memoria local del subproceso B también se convierte en 1.
Visto en su conjunto, estos dos pasos son esencialmente el hilo A que envía mensajes al hilo B, y este proceso de comunicación debe pasar por la memoria principal. JMM proporciona garantías de visibilidad de memoria para los programadores de Java al controlar la interacción entre la memoria principal y la memoria local de cada subproceso.

Cuarto, la operación interactiva entre la memoria.

La interacción entre la memoria principal y la memoria de trabajo define 8 operaciones atómicas. Los detalles son los siguientes:

  • lock: una variable que actúa en la memoria principal e identifica una variable como un estado exclusivo de subproceso

  • desbloqueo (desbloqueo): una variable que actúa en la memoria principal y libera una variable que está en estado bloqueado

  • leer (leer): una variable que actúa en la memoria principal, transfiere el valor de una variable desde la memoria principal a la memoria de trabajo del hilo

  • carga: una variable que actúa en la memoria de trabajo, coloca o copia el valor de la variable transferida de lectura a una copia de la variable en la memoria de trabajo

  • use (use): una variable que actúa en la memoria de trabajo, lo que significa que el hilo se refiere al valor de la variable en la memoria de trabajo y pasa el valor de una variable en la memoria de trabajo al motor de ejecución

  • asignar (asignación): una variable que actúa en la memoria de trabajo, lo que indica que el hilo asigna el valor especificado a una variable en la memoria de trabajo.

  • almacenar (almacenamiento): una variable que actúa en la memoria de trabajo y transfiere el valor de una variable en la memoria de trabajo a la memoria principal

  • escribir: escribe en la variable en la memoria principal, coloca el valor de la variable pasado por la tienda en la variable correspondiente en la memoria principal

La siguiente imagen puede ayudarnos a profundizar nuestra impresión.
Inserte la descripción de la imagen aquí

Cinco, la diferencia entre volátil y sincronizado

Primero, necesitamos comprender dos aspectos de la seguridad del hilo: control de ejecución y visibilidad de la memoria.

El propósito del control de ejecución es controlar la ejecución del código (secuencia) y si puede ejecutarse simultáneamente.

La visibilidad de la memoria controla la visibilidad de los resultados de ejecución de subprocesos a otros subprocesos en la memoria. Según la implementación del modelo de memoria Java, cuando el subproceso se ejecuta específicamente, primero copiará los datos de la memoria principal al subproceso local (caché de la CPU) y luego vaciará el resultado del subproceso local a la memoria principal después de que se complete la operación.

La palabra clave sincronizada resuelve el problema del control de ejecución: evita que otros subprocesos adquieran el bloqueo de supervisión del objeto actual, de modo que otros subprocesos no puedan acceder al bloque de código protegido por la palabra clave sincronizada en el objeto actual y no se puede ejecutar simultáneamente. Más importante aún, sincronizado también creará una barrera de memoria. La instrucción de barrera de memoria asegura que todos los resultados de la operación de la CPU se enjuaguen directamente a la memoria principal, asegurando así la visibilidad de la memoria de la operación, y también hace que todos los hilos que primero adquieren este bloqueo Operación, sucede antes en la operación del hilo que posteriormente adquirió el bloqueo.

Para comprender lo que sucede antes, consulte este artículo [Principios importantes de concurrencia] y la comprensión de lo que sucede antes

La palabra clave volátil resuelve el problema de la visibilidad de la memoria, de modo que todas las lecturas y escrituras en variables volátiles se transferirán directamente a la memoria principal, lo que garantiza la visibilidad de las variables. De esta manera, puede cumplir con algunos requisitos que requieren visibilidad variable y sin requisitos de orden de lectura.

El uso de la palabra clave volátil solo puede darse cuenta de la atomicidad de las operaciones en las variables originales (como boolen, short, int, long, etc.), pero necesita atención especial de que volátil no puede garantizar la atomicidad de las operaciones compuestas.

Para la palabra clave volátil, se puede usar si y solo si se cumplen todas las condiciones siguientes:

  1. Escribir en una variable no depende del valor actual de la variable, o puede asegurarse de que solo un solo subproceso actualice el valor de la variable.
  2. La variable no se incluye en la invariante con otras variables.

La diferencia entre volátil y sincronizado

  • La esencia de volátil es decirle a jvm que el valor de la variable actual en el registro (memoria de trabajo) es incierto y debe leerse desde la memoria principal; sincronizado es bloquear la variable actual, solo el hilo actual puede acceder a la variable y otros hilos están bloqueados .
  • volátil solo se puede usar a nivel variable; sincronizado se puede usar a nivel de variable, método y clase
  • Volátil solo puede lograr la visibilidad de modificación de las variables, y no puede garantizar la atomicidad; mientras que sincronizado puede garantizar la visibilidad de modificación y la atomicidad de las variables
  • volátil no causa bloqueo de hilo; sincronizado puede causar bloqueo de hilo.
  • Las variables marcadas con volátil no serán optimizadas por el compilador; las variables marcadas con sincronizado pueden ser optimizadas por el compilador.

Este artículo se refiere a: Conocimiento profundo del modelo de memoria Java (1) -fundación

Si desea comprender el peligro del patrón de bloqueo de doble verificación, consulte este artículo: [Modo de caso único] Problemas y soluciones de DCL

147 artículos originales publicados · 70 alabanzas · 30,000+ vistas

Supongo que te gusta

Origin blog.csdn.net/TreeShu321/article/details/105470007
Recomendado
Clasificación