Breve introducción de la palabra clave volátil

Si va a una entrevista, el entrevistador le pregunta, hable sobre su comprensión de volátil.
Después de leer este blog, creo que puedes responder con calma.

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

Tres características de volátiles:

  • Asegurar la visibilidad
  • Sin garantía de atomicidad
  • Prohibir la reordenación de pedidos

Entonces, ¿qué significan estas tres características?

Asegurar la visibilidad

Primero introduzca un concepto:
modelo de memoria JMM (Java Memory Model, JMM), en sí mismo es un concepto abstracto que realmente no existe, describe un conjunto de reglas o especificaciones, a través de este conjunto de especificaciones se definen las variables en el programa ( Incluyendo campos de instancia, campos estáticos y elementos que componen un objeto de matriz) métodos de acceso, disposiciones de JMM sobre sincronización:

  1. Antes de que se desbloquee el hilo, el valor de la variable compartida debe volver a la memoria
  2. Antes de que el hilo se bloquee, debe leer el último valor de la memoria principal en su memoria de trabajo
  3. Bloquear y desbloquear es usar un candado

A continuación, mire la imagen para comprender que no es difícil:
Inserte la descripción de la imagen aquí
porque la entidad de la memoria de ejecución de la JVM es un subproceso, y cuando se crea cada subproceso, la JVM creará una memoria de trabajo (espacio de pila) para él, y la memoria de trabajo es un área privada de cada subproceso. El modelo de memoria Java estipula que todas las variables se almacenan en la memoria principal. La memoria principal es un área de memoria compartida, a la que pueden acceder todos los subprocesos. Sin embargo, las operaciones de los subprocesos sobre las variables (asignación de lectura, etc.) deben realizarse en la memoria de trabajo. Copie desde la memoria principal a su propio espacio de memoria de trabajo y luego opere en las variables. Una vez completada la operación, escriba las variables en la memoria principal. No puede manipular directamente las variables en la memoria principal. La memoria de trabajo en cada subproceso se almacena en la memoria principal. Una copia de la copia de la variable, por lo que los diferentes hilos no pueden acceder a la memoria de trabajo del otro, y la comunicación (transferencia de valor) entre hilos debe completarse a través de la memoria principal.

Cuando la variable utiliza la palabra clave volátil, se puede garantizar que una vez que el hilo actual modifica el valor copiado de la memoria principal (en este momento el valor se copia a la memoria privada del hilo actual), puede ser copiado inmediatamente por otros valores de la memoria principal. El hilo lo sabe. (Asegúrese de que cada hilo pueda obtener el último valor de la memoria principal (la variable es modificada por volátil))

Verificación del código de visibilidad:
primero se necesita una clase de recurso

class Data {
    
    
    int number = 0;
    public void add() {
    
    
        this.number = 60;
    }
}

Luego, dos subprocesos operan el recurso al mismo tiempo, un subproceso principal, un subproceso nuevo

/**
 * @author Cocowwy
 * @create 2020-09-09-22:33
 */
public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
    	Data data = new Data();
        //线程thread1
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
    
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            data.add();
            System.out.println(Thread.currentThread().getName() + "\t update number");
        }, "thread1").start();
		
		//线程main
        while (data.number == 0) {
    
    
            //当number的值一直为0则死循环
        }

        System.out.println(Thread.currentThread().getName() + "\t mission is down main get number:" + data.number);
     }
}

Los resultados son los siguientes:
Inserte la descripción de la imagen aquí
A continuación, analizamos brevemente el programa anterior:
Primero, la clase de recurso Fecha define un número de variable, exponiendo un método para modificar este valor, luego miramos nuestro principal, el principal es un hilo y luego hay un nuevo hilo hilo1 , El subproceso principal tiene un bucle infinito. Cuando el valor de los datos llega a 60, se ha bloqueado y el subproceso thread1 modifica el valor de estos datos. Sin embargo, después de la modificación, el subproceso principal todavía está bloqueado en el ciclo while.
A través del modelo de memoria JMM y la visibilidad introducida anteriormente, podemos saber que los datos copiados por el hilo principal siempre son 0, y no hay cambios después de que el hilo thread1 modifica este valor.

A continuación, solo modifico la clase de recurso y agrego la palabra clave volátil:

class Data {
    
    
    volatile int number = 0;
    public void add() {
    
    
        this.number = 60;
    }
}

El resultado es el siguiente: De
Inserte la descripción de la imagen aquí
esta manera podemos ver que el bucle infinito se suelta porque el valor del número cambia.

Así podemos saber que volatile puede garantizar la visibilidad, incluso si se notifica a otros hilos que se ha modificado el valor de la memoria física principal

Sin garantía de atomicidad

En primer lugar, debemos comprender qué es la atomicidad.

Atomicidad significa indivisibilidad e integridad, es decir, cuando un hilo está haciendo un negocio específico, no se puede bloquear ni dividir por la mitad, necesita ser integral.

O triunfar al mismo tiempo o fracasar al mismo tiempo.
De acuerdo con la introducción de JMM, puede haber un subproceso A que modifica el valor de la variable compartida x pero no se ha escrito de nuevo en la memoria principal, y otro subproceso BBB opera en la misma variable compartida x en la memoria principal, pero en este momento el subproceso AAA La variable compartida x en la memoria de trabajo no es visible para el hilo B. Este retraso en la sincronización entre la memoria de trabajo y la memoria principal causa problemas de visibilidad. (Es decir, la operación de obtener datos e insertarlos de nuevo en la memoria principal repetidamente no es una operación atómica)

Luego, use el código para demostrar que volatile no garantiza la atomicidad:
Primero, la clase de recurso, que expone un método para proporcionar autoincremento:

class Data {
    
    
    volatile int number = 0;
    public  void  addPlus() {
    
    
        number++;
    }
}

La próxima vez mian:

public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
        Data data = new Data();
        for (int i = 1; i <= 10; i++) {
    
    
            new Thread(() -> {
    
    
                for(int j=0;j<1000;j++){
    
    
                    //上面为验证原子性  下面为使用AtomicInteger来保证原子性的操作
                    data.addPlus();
                }
            }, "Thread" + i).start();
        }

        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少

        // 一个是main线程 一个是gc线程
        while (Thread.activeCount()>2){
    
    
            Thread.yield();
        }
        // 两种方法均可
//        try {
    
    
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
    
    
//            e.printStackTrace();
//        }

        System.out.println(Thread.currentThread().getName()+"线程获取的number值为"+data.number);
    }
}

El resultado es el siguiente:
Inserte la descripción de la imagen aquí
A partir del código anterior, podemos saber que hay 10 subprocesos, cada subproceso ha realizado 1000 veces operaciones de autoincremento en los recursos y se agrega volátil para garantizar la visibilidad, pero el resultado es menor a 10000, lo que verifica No se puede garantizar la atomicidad y existe un retraso de sincronización entre la memoria de trabajo y la memoria principal.

Entonces, ¿cómo solucionar este problema de visibilidad atómica?
La primera solucion:

class Data {
    
    
    volatile int number = 0;
    public synchronized  void addPlus() {
    
    
        number++;
    }
}

Inserte la descripción de la imagen aquí

El primer método es, naturalmente, agregar sincronizado para bloquear el método, pero se puede usar otro método aquí.

La segunda solución:
use clases atómicas, AtomicInteger bajo JUC

class Data {
    
    
    /**
     * 原子类
     */
    AtomicInteger atomicInteger=new AtomicInteger(0);
    
    /**
     * 原子的自增操作
     */
    public void addAtomicInteger(){
    
    
        atomicInteger.getAndIncrement();
    }
}

Podemos mirar la API:
Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí
y luego cambiar la principal:

public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
        Data data = new Data();
        for (int i = 1; i <= 10; i++) {
    
    
            new Thread(() -> {
    
    
                for(int j=0;j<1000;j++){
    
    
                    //上面为验证原子性  下面为使用AtomicInteger来保证原子性的操作
                    data.addAtomicInteger();
                }
            }, "Thread" + i).start();
        }

        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少
        // 一个是main线程 一个是gc线程
        while (Thread.activeCount()>2){
    
    
            Thread.yield();
        }
        // 两种方法都行
//        try {
    
    
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
    
    
//            e.printStackTrace();
//        }

        System.out.println(Thread.currentThread().getName()+"线程获取的atomicInteger值为"+data.atomicInteger);
    }
}

El resultado es el siguiente:
Inserte la descripción de la imagen aquí
Entonces podemos garantizar la atomicidad sincronizando o usando AtomicInteger.

Reorganización de pedidos

Cuando la JVM ejecuta el código escrito, para mejorar el rendimiento, tanto el compilador como el procesador reordenarán las instrucciones compiladas. Dividido en 3 tipos:

  1. Reorganización de la optimización del compilador: la premisa de optimización del compilador es reorganizar el orden de ejecución de las declaraciones sin cambiar la semántica de un solo subproceso.
  2. Reorganización de instrucciones paralelas: si no hay dependencia de datos entre ciertas instrucciones en el código, el procesador puede cambiar el orden de las instrucciones correspondientes a las instrucciones de la máquina
  3. También hay una caché de segundo y tercer nivel entre el procesador y la memoria principal. La existencia de estos cachés de lectura y escritura hace que las operaciones de acceso y carga de programas no estén en orden.

Bajo la condición de un solo hilo, la reordenación de instrucciones no afectará el resultado final, es decir, se puede garantizar la consistencia de los datos; pero bajo la condición de varios hilos, varios hilos se ejecutan alternativamente, y las variables utilizadas entre dos hilos pueden ser Si se garantiza la coherencia, no se puede garantizar.

A continuación, introduzca brevemente el uso de volatile en modo singleton:

Todos sabemos que la implementación común del patrón singleton incluye el estilo de hombre hambriento y el estilo de hombre perezoso, pero de hecho hay hasta 7-8 formas de implementar el modo singleton. Puedes consultar este artículo mío: Modo Singleton de Java Design Patterns

El modo singleton que usa volatile es:

DCL (mecanismo de bloqueo de doble verificación)

el código se muestra a continuación:

/**
 * @author Cocowwy
 * @create 2020-09-09-18:46
 */
public class SingletonDemo {
    
    

    /**
     * 排除指令重排的情况
     */
    private volatile static SingletonDemo instance = null;

    private SingletonDemo() {
    
    
        System.out.println("初始化构造器!");
    }

    /**
     * 双重锁校验 加锁前和加锁后都进行判断
     */
    public static SingletonDemo getInstance() {
    
    
        if (null == instance) {
    
    
            synchronized (SingletonDemo.class) {
    
    
                if (null == instance) {
    
    
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
    
    

        for (int i = 0; i < 10; i++) {
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    SingletonDemo.getInstance();
                }
            }, i + "").start();
        }
    }
}

El resultado es el siguiente:
Inserte la descripción de la imagen aquí
podemos ver que el constructor se inicializa solo una vez, lo que significa que este es un singleton que obtiene un hilo.

Supongo que te gusta

Origin blog.csdn.net/Pzzzz_wwy/article/details/108610919
Recomendado
Clasificación