Sabemos que las operaciones concurrentes implementadas por Java deben ser finalmente completadas por nuestra CPU. Mientras tanto, compilamos el código fuente de Java en un archivo .class, luego cargamos, y luego lo ejecutamos por el motor de ejecución de la máquina virtual, interpretado como lenguaje de ensamblaje, luego convertidos en instrucciones del sistema operativo, luego convertido a 1, 0, y finalmente la CPU se reconoce y ejecuta.
Cuando mencionamos la concurrencia de Java, no podemos evitar pensar en palabras clave comunes en Java: volátiles y sincronizadas. A continuación, las analizaremos de estas dos palabras de cierre:
El principio de implementación subyacente de volátil
Principios de implementación y aplicaciones de sincronizado
volátil
Hablando de volátil, el entrevistador es la pregunta favorita para hacer en entrevistas de Java. Cuando lo vemos, lo primero que pensamos es mantener la visibilidad entre los hilos. Es un sincronizado liviano, que en algunos casos puede reemplazar sincronizado.
El papel de volátil:
Para una variable modificada por Volatile, el modelo de memoria Java asegurará que los valores variables vistos por todos los subprocesos sean consistentes.
Cómo funciona volátil:
Podemos definir una variable volátil, asignar valores y usar herramientas para obtener las instrucciones de ensamblaje generadas por el compilador JIT. Encontraremos que al escribir en la variable volátil, habrá una instrucción adicional: una instrucción prefijo con bloqueo:
La instrucción de prefijo de bloqueo provoca dos cosas al procesador de múltiples núcleos:
① Escriba los datos de la línea de caché del procesador actual a la memoria.
② Esta operación de memoria de retroceso de escritura invalidará la dirección de memoria en caché de datos en otras CPU.
Cuando conocemos los dos puntos anteriores, no es difícil para nosotros comprender el mecanismo de la variable Volatie.
En múltiples procesadores, para garantizar que el caché de cada procesador sea consistente, se implementará un protocolo de consistencia de caché. Cada procesador huele los datos propagados en el bus para verificar si el valor en caché ha expirado.
sincronizado
Al pensar en la concurrencia múltiple, lo primero que pienso es sincronizar. Traducido como sincronización. Todos sabemos que es una cerradura de peso pesado. Al usarlo para un método o bloque de código, cuando un hilo obtiene este bloqueo, otros hilos caerán en un estado suspendido, que aparecerá en el estado de sueño en Java. Todos sabemos que la suspensión del hilo y el tiempo de ejecución deben transferirse al estado del núcleo del sistema operativo (el estado del núcleo correspondiente es el estado del usuario), que es particularmente un desperdicio de recursos de CPU, ¡por lo que este bloqueo de peso pesado es verdadero!
Sin embargo, después de Java SE 1.6, el equipo de mantenimiento de Java ha realizado una serie de optimizaciones (estas optimizaciones se discuten una por una), por lo que no es tan "pesado", y el bloqueo reentrante, que tenía ventajas en el pasado, se ha vuelto menos ventajoso (reentrenado).
Hablemos de sincronizado en los siguientes aspectos:
Los conceptos básicos de sincronizado para lograr la sincronización
Cómo sincronizado implementa bloquear
Bloqueo positivo, bloqueo liviano (bloqueo de giro), bloqueo de peso pesado
Actualización de bloqueo
Cómo implementar operaciones atómicas en Java
①Los conceptos básicos de sincronizado para lograr la sincronización:
Podemos ver sincronizado en el desarrollo o en el código fuente de Java, como hashtable, StringBuilder y otros lugares. Hay dos formas comunes:
Ⅰ, método de sincronización
El método de sincronización solo debe sincronizarse antes del método. Cuando un hilo lo ejecuta, otros hilos caerán en espera hasta que libere el bloqueo. El uso de métodos se puede dividir en dos tipos: para métodos de sincronización ordinarios y para métodos estáticos. La diferencia entre ellos es que los objetos bloqueados son diferentes. La posición bloqueada de los métodos ordinarios es el objeto actual, y la posición bloqueada de los métodos estáticos es el objeto de clase de la clase actual.
Ⅱ, bloque de método de sincronización
El bloque del método de sincronización bloquea el objeto configurado en los soportes después de sincronizar. Este objeto puede ser un valor y cualquier variable u objeto.
② Cómo sincronizado implementa bloqueo:
En la especificación JVM, puede ver el principio de implementación de sincronizado en JVM. JVM implementa la sincronización de los métodos de sincronización y los bloques de código basados en ingresar y salir del objeto Monitor. El bloque de código se implementa utilizando instrucciones monitoreadoras y monitorexit. El método de sincronización no se da específicamente en la especificación JVM. Sin embargo, creo que los principios específicos deberían ser diferentes. No es más que compilar el código fuente de Java en un archivo de clase y marcar el método sincronizado en el archivo de Bytecode de clase. Este método se sincronizará cuando el motor Bytecode ejecute este método.
③ Claque de deflexión, bloqueo liviano (bloqueo de giro), bloqueo de peso pesado:
Antes de hablar de cerraduras, necesitamos conocer el encabezado del objeto Java y el encabezado del objeto Java:
El bloqueo utilizado por sincronizado se almacena en el encabezado del objeto Java. El encabezado del objeto Java tiene 32 bits/64 bits (dependiendo del número de bits del sistema operativo) Longitud Markword almacena la información hashcode y bloquea del objeto. Hay espacios de 2 bits en Markword para representar el estado del bloqueo 00, 01, 10, 11, respectivamente, que representan cerraduras livianas, cerraduras de sesgo, cerraduras de peso pesado y marcas GC.
Bloqueo positivo: el bloqueo positivo se llama bloqueo excéntrico. Por el nombre podemos ver que es un bloqueo que tiende hacia un cierto hilo.
En el desarrollo real, descubrimos que la concurrencia de múltiples subprocesos, la mayoría de los métodos de sincronización son realizados por el mismo hilo, y la probabilidad de que múltiples hilos compitan por un método es relativamente baja, por lo que la adquisición repetida y la liberación de bloqueos causarán muchos desechos de recursos. Por lo tanto, para que el hilo obtenga un bloqueo a un costo más bajo, se introduce el bloqueo de sesgo. Cuando un hilo accede a un bloque de sincronización y adquiere un bloqueo, la ID de rosca del bloqueo de polarización se almacenará en el registro de bloqueo en el marco de la pila del encabezado del objeto y el hilo. En el futuro, cuando el hilo entra y sale del bloque de sincronización, no necesita realizar operaciones CAS para bloquear y desbloquear. Solo es necesario simplemente verificar si hay un bloqueo de polarización que apunta a la palabra de marca actual en el encabezado del objeto (en Markword, hay un bit de bloqueo de bloqueo de polarización para indicar si el objeto actual es compatible con el bloqueo de polarización. Podemos usar el parámetro JVM para establecer el bloqueo de polarización).
Con respecto a la liberación de cerraduras sesgadas, las cerraduras sesgadas usan el mecanismo de liberar el bloqueo hasta que exista la competencia, por lo que el hilo que sostiene el bloqueo sesgado liberará el bloqueo cuando otros hilos intenten competir por bloqueos sesgados.
Nota: En Java6, 7, el bloqueo de polarización se inicia de forma predeterminada
Lock de peso ligero:
Un bloqueo liviano es que antes de ejecutar el bloque de sincronización, JVM creará un espacio para almacenar el registro de bloqueo en el marco de la pila del hilo actual y copiará la palabra de marca en el encabezado del objeto. Luego, el hilo intentará reemplazar la palabra Mark en el encabezado del objeto con un puntero al registro de bloqueo. Si es exitoso, el hilo actual obtiene el bloqueo. Si falla, significa que otros hilos compiten por el bloqueo, y el hilo actual girará para obtener el bloqueo.
④ Actualización de bloqueo:
Si el hilo actual no puede probar el método anterior para obtener el bloqueo, significa que el bloqueo actual está en competencia y el bloqueo se actualizará a un bloqueo de peso pesado.
La diferencia entre el bloqueo liviano y el bloqueo sesgado:
Las cerraduras livianas usan operaciones CAS para eliminar los mutexes utilizados en la sincronización sin competencia, mientras que los bloqueos sesgados eliminan toda la sincronización sin competencia sin competencia, ¡e incluso las operaciones de CAS no se realizan!
⑤ Cómo implementar operaciones atómicas en Java:
Antes de comprender cómo Java implementa operaciones atómicas, necesitamos saber cómo los procesadores implementan operaciones atómicas:
Los procesadores generalmente se dividen en dos formas de realizar operaciones atómicas: bloqueo de caché y bloqueo de bus, entre los cuales el bloqueo de caché es mejor, mientras que el bloqueo de bus es más consumo de recursos. (No explicaremos demasiado sobre los dos métodos de bloqueo aquí, pero habrá explicaciones detalladas en el sistema operativo)
Java usa (en su mayoría) CAS de bucle para implementar operaciones atómicas, pero el uso de CAS para implementar operaciones atómicas también causará algunos de los siguientes problemas clásicos:
1) Problema de ABA
AtomicStampedReference se proporciona en JDK para resolver (proporcionando verificación de referencias esperadas y banderas esperadas)
2) Tiempo de ciclo largo y alto gasto
No se puede resolver esto, este es un problema común de circulación
3) Solo se pueden garantizar operaciones atómicas de una variable compartida
Se proporciona una atomicreferencia en JDK para resolver el problema, colocando múltiples variables compartidas en una clase para operaciones CAS.