juc--Resumen de los problemas centrales de la programación concurrente ③

Continuación del artículo anterior juc – Resumen de los temas centrales de la programación concurrente ②

1. Devolución de llamada asíncrona

1. ¿Qué es la devolución de llamada asíncrona?

El más común que solemos usar es la devolución de llamada síncrona. La devolución de llamada síncrona se bloqueará y un solo subproceso debe esperar el retorno del resultado para continuar con la ejecución.
inserte la descripción de la imagen aquí

Supongamos que hay dos tareas A y B. La tarea A necesita usar la tarea B para calcular los resultados. Hay dos formas de lograrlo:

  1. A y B se ejecutan secuencialmente en el mismo hilo. Es decir, ejecute B primero y luego ejecute A después de obtener el resultado devuelto.

  2. A y B se ejecutan en paralelo. Cuando A necesita el resultado del cálculo de B, si B no ha terminado de ejecutar, A puede hacer otro trabajo primero para evitar el bloqueo y preguntarle a B nuevamente después de un período de tiempo.

Podemos escribir directamente un método en A para procesar el resultado procesado por B, y luego llamar al método A después de que se procese B.

inserte la descripción de la imagen aquí

2. Java implementa devoluciones de llamadas asincrónicas

Implementado a través de la interfaz Future

La clase Future existe en el paquete concurrente del JDK, y su objetivo principal es recibir los resultados devueltos por el cálculo de subprocesos asíncronos de Java.
java.util.concurrent.Future

  • Ejecución asíncrona
  • devolución de llamada exitosa
  • devolución de llamada fallida
public class FutureDemo {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 发送请求 Void:没有返回结果
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
    
    
            try {
    
    
                // 模仿耗时的操作
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"runAsync-》Void");
        });

        System.out.println(Thread.currentThread().getName()+"线程");
    }
}

Solo se ejecuta el hilo principal.
inserte la descripción de la imagen aquí

public class FutureDemo {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 发送请求 Void:没有返回结果
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
    
    
            try {
    
    
                // 模仿耗时的操作
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"runAsync-》Void");
        });

        System.out.println(Thread.currentThread().getName()+"线程");

        //获取阻塞执行结果
        Void result = future.get();
    }
}

inserte la descripción de la imagen aquí

// 有返回值的异步回调 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
    
    
     System.out.println(Thread.currentThread().getName()+"runAsync-》Integer");
     //成功则返回1024
     return 1024;
 });

System.out.println(future.whenComplete((t, u) -> {
    
    
    System.out.println("t:" + t + "  u:" + u);
}).exceptionally((e) -> {
    
    
    System.out.println(e.getMessage());
    //失败则返回233
    return 233;
}).get());

inserte la descripción de la imagen aquí
Agregue una línea de código de error:

// 有返回值的异步回调 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
    
    
    System.out.println(Thread.currentThread().getName()+"runAsync-》Integer");
    int i = 2/0;
    //成功则返回1024
    return 1024;
});

System.out.println(future.whenComplete((t, u) -> {
    
    
    System.out.println("t:" + t + "  u:" + u);
}).exceptionally((e) -> {
    
    
    System.out.println(e.getMessage());
    //失败则返回233
    return 233;
}).get());
}

inserte la descripción de la imagen aquí

2. JMM

JMM-----Modelo de memoria Java

En java, todos los campos de instancia, campos estáticos y elementos de matriz se almacenan en la memoria de almacenamiento dinámico, que se comparte entre subprocesos.
Las variables locales, los parámetros de definición de métodos y los parámetros del controlador de excepciones no se comparten entre subprocesos, no tienen problemas de visibilidad de la memoria y no se ven afectados por el modelo de memoria.

JMM define una relación abstracta entre los subprocesos y la memoria principal: las variables compartidas entre subprocesos se almacenan en la memoria principal, y cada subproceso tiene una memoria local privada que almacena una copia de las variables compartidas de lectura/escritura del subproceso. La memoria local es una abstracción del JMM que en realidad no existe.

Convención de JMM:

  1. Antes de que se desbloquee el subproceso, la variable compartida debe estarinmediatamenteVuelve a la memoria principal.
    inserte la descripción de la imagen aquí
  2. Antes de que un subproceso se bloquee, debe leer el último valor de la memoria principal en la memoria de trabajo.
  3. El bloqueo y el desbloqueo son el mismo bloqueo.

inserte la descripción de la imagen aquí
Hay 8 tipos de operaciones de interacción de memoria, y la implementación de la máquina virtual debe garantizar que cada operación seaátomo, indivisible (para variables de tipo doble y largo, las operaciones de carga, almacenamiento, lectura y escritura permiten excepciones en algunas plataformas)

  • lock (bloqueo): una variable que actúa en la memoria principal e identifica una variable como exclusiva del subproceso.
  • desbloquear (desbloquear): una variable que actúa en la memoria principal, libera una variable en un estado bloqueado, y la variable liberada puede ser bloqueada por otros hilos.
  • read (leer): actúa sobre la variable de memoria principal, transfiere el valor de una variable de la memoria principal a la memoria de trabajo del hilo para que lo utilice la acción de carga posterior.
  • load (carga): una variable que actúa sobre la memoria de trabajo, coloca la operación de lectura de la variable en la memoria principal en la memoria de trabajo.
  • use (use): Actúa sobre variables en memoria de trabajo, transmite variables en memoria de trabajo al motor de ejecución, siempre que la máquina virtual encuentre un valor que necesite usar una variable, usará esta instrucción.
  • asignar (asignación): actúa sobre una variable en la memoria de trabajo, pone un valor recibido del motor de ejecución en una copia de la variable en la memoria de trabajo.
  • store (almacenamiento): actúa sobre una variable en la memoria principal, transfiere un valor de una variable en la memoria de trabajo a la memoria principal para su posterior uso de escritura.
  • write (escribir): actúa sobre la variable en memoria principal, pone el valor de la variable obtenido de la memoria de trabajo por la operación de almacenamiento en la variable en la memoria principal.

JMM ha formulado las siguientes reglas para el uso de estas ocho instrucciones:

  • Una de las operaciones de lectura y carga, almacenamiento y escritura por sí sola no está permitida. Es decir, si usa leer, debe cargar, y si usa almacenar, debe escribir.
  • El hilo no puede descartar su operación de asignación más reciente, es decir, después de que los datos de la variable de trabajo hayan cambiado, debe informar a la memoria principal.
  • No se permite que un subproceso sincronice datos no asignados de la memoria de trabajo a la memoria principal.
  • Se debe crear una nueva variable en la memoria principal, y la memoria de trabajo no puede usar directamente una variable no inicializada. Es decir, antes de implementar las operaciones de uso y almacenamiento en la variable, debe pasar por las operaciones de asignación y carga.
  • Solo un subproceso puede bloquear una variable a la vez. Después de varios bloqueos, se debe ejecutar el mismo número de desbloqueos para desbloquear.
  • Si la operación de bloqueo se realiza en una variable, se borrarán todos los valores de la variable en la memoria de trabajo. Antes de que el motor de ejecución pueda usar la variable, el valor de la variable debe inicializarse mediante la operación de recarga o asignación.
  • Si una variable no está bloqueada, no se puede desbloquear. Tampoco puede desbloquear una variable que está bloqueada por otro hilo.
  • Antes de desbloquear una variable, la variable debe sincronizarse con la memoria principal.

Si el subproceso A modifica el valor de la variable compartida, pero la variable en la memoria local del subproceso B sigue siendo el valor anterior, es decir, el subproceso B no puede verlo a tiempo. ¿Cómo hacerle saber a B que el valor en la memoria principal ha cambiado? Usa volátiles.

3*, volátil

Volatile es un mecanismo de sincronización ligero proporcionado por la máquina virtual de Java.

  • Visibilidad garantizada
  • La atomicidad no está garantizada.
  • Deshabilitar el reordenamiento de instrucciones

Vea una comprensión profunda de las características de volátil en Java

4. CAS detallado

¿Qué es CAS?
CAS es la abreviatura de Compare and Swap, que es comparar y reemplazar.

Hay 3 operandos básicos utilizados en el mecanismo CAS:dirección de memoria V, antiguo valor esperado A, nuevo valor B a modificar.
Al actualizar una variable, solo cuando el valor esperado A de la variable y el valor real en la dirección de memoria V sean iguales, el valor correspondiente a la dirección de memoria V se cambiará a B.

Suponiendo que hay varios subprocesos que realizan operaciones CAS y que hay muchos pasos CAS, es posible que después de juzgar que V y E son iguales, cuando están a punto de asignar valores, intercambien subprocesos y cambien el valor. ¿Causó la inconsistencia de los datos?
La respuesta es no, porque CAS es una primitiva de sistema, la primitiva pertenece a la categoría de términos de sistema operativo, está compuesta por varias instrucciones y se utiliza para completar un proceso de una función, y la ejecución de la primitiva debe ser continua. No se permite que se interrumpa durante el proceso de ejecución, es decir, CAS es una instrucción atómica de la CPU, lo que no causará el llamado problema de inconsistencia de datos.

public class CASDemo {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger atomicInteger = new AtomicInteger(999);
        //compareAndSet(int expect, int update)
        System.out.println(atomicInteger.compareAndSet(999, 666));//true

        System.out.println(atomicInteger.get());//666
    }
}

Preguntas ABA:
Si una variable V tiene el valor A cuando se lee por primera vez, y cuando está lista para ser asignada, se comprueba que todavía tiene el valor A, ¿significa que su valor no ha cambiado? No, debido a que su valor puede modificarse a un valor B durante este período de tiempo y luego volver a modificarse a A, entonces CAS pensará erróneamente que nunca ha cambiado.

Para resolver este problema, el paquete JUC proporciona una clase de referencia atómica marcada AtomicStampedReference, que puede garantizar la corrección de CAS al controlar la versión del valor de la variable. Sin embargo, esta categoría es relativamente inútil.

Desventajas de CAS:

  1. Problema ABA.
  2. Lo que garantiza el mecanismo CAS es solo la operación atómica de una variable, no la atomicidad de todo el bloque de código. Por ejemplo, si necesita asegurarse de que tres variables se actualicen atómicamente juntas, debe usar Sincronizado.
  3. La capa inferior de CAS es un bloqueo de giro En el caso de alta concurrencia, si muchos subprocesos intentan actualizar una variable repetidamente, pero la actualización no tiene éxito, el ciclo se repite, lo que ejercerá mucha presión sobre la CPU.

Supongo que te gusta

Origin blog.csdn.net/myjess/article/details/121392658
Recomendado
Clasificación