Un resumen de todos los "bloqueos" en Java, nunca tendrás miedo de encontrar bloqueos en futuras entrevistas.

1. Introducción

En primer lugar, los bloqueos de Java se dividen en dos categorías:

  1. El primer tipo es la   palabra clave sincronizada , que es un bloqueo implícito, que se implementa en el nivel de JVM y no se puede ver cuando se usa;

  2. La segunda categoría es la  interfaz de bloqueo agregada después de jdk5  y varias clases de implementación correspondientes. Este es un bloqueo explícito, es decir, podemos ver el objeto de bloqueo en el nivel de código, y la implementación del método de estos objetos depende principalmente directamente La instrucción de la CPU no tiene nada que ver con la implementación de jvm.

A continuación, hablaremos de  sincronizado  y  bloqueo  .

二 、 sincronizado

2.1 El uso de sincronizados

  • Si la modificación es 具体对象: el bloqueo es 对象;

  • Si la modificación es 成员方法: entonces el bloqueo es  this ;

  • Si la modificación es 静态方法: este es el candado 对象.class.

2.2 Monitor y encabezado de objeto Java

Antes de comprender el principio de sincronizado, debemos agregar conocimientos sobre los objetos java.

El diseño de un objeto en la memoria se divide en tres áreas: encabezado del objeto, datos de instancia y relleno de alineación .

  1. Cabeza de objeto . La parte del encabezado del objeto del objeto de la máquina virtual Hot Spot incluye dos tipos de información. El primer tipo se utiliza para almacenar los datos de tiempo de ejecución del objeto en sí, como el código hash, la edad de generación de GC, el indicador de estado de bloqueo, el bloqueo retenido por el hilo, el ID del hilo sesgado, la marca de tiempo sesgada, etc., esta parte de los datos La longitud de es de 32 bits y 64 bits respectivamente en máquinas virtuales de 32 y 64 bits (sin punteros comprimidos) Oficialmente se llama "Marca de palabra".

El objeto necesita almacenar una gran cantidad de datos en tiempo de ejecución, que en realidad exceden el registro máximo de la estructura de mapa de bits de 32 y 64 bits. Sin embargo, la información en el encabezado del objeto es un costo de almacenamiento adicional que no tiene nada que ver con los datos definidos por el objeto en sí. Teniendo en cuenta la máquina virtual Eficiencia de espacio, Mark Word está diseñado como una estructura de datos definida dinámicamente para almacenar la mayor cantidad de datos posible en un espacio muy pequeño y reutilizar su propio espacio de almacenamiento de acuerdo con el estado del objeto.

  1. Datos de instancia . La parte de datos de instancia es la información efectiva realmente almacenada por el objeto, es decir, se debe registrar el contenido de varios tipos de campos definidos en el código del programa, ya sean heredados de la clase principal o los campos definidos en la subclase.

El orden de almacenamiento de esta parte se verá afectado por el parámetro de estrategia de asignación de la máquina virtual (-XX: parámetro de estilo de asignación de campos) y el orden en el que se definen los campos en el código fuente de Java. El orden de asignación predeterminado de la máquina virtual Hot Spot es longs / doubles, ints, short / chars, bytes / booleanos, oops (Ordinary Object Pointers, OOP). De la estrategia de asignación predeterminada anterior, puede ver que los campos con el mismo ancho son siempre Asignarlos juntos y almacenarlos Si se cumple este requisito previo, las variables definidas en la clase padre aparecerán antes que la clase hijo. Si el valor del parámetro XX: Compact Fields de la máquina virtual Hotspot es verdadero (el valor predeterminado es verdadero), las variables más estrechas de la subclase también se pueden insertar en el espacio de la variable de clase principal para ahorrar un poco de espacio.

  1. Alinear y rellenar . No existe necesariamente, porque el sistema automático de administración de memoria de la máquina virtual Hotspot requiere que la dirección de inicio del objeto sea un múltiplo entero de 8 bytes. Si la parte de datos de la instancia del objeto no está alineada, debe completarse con relleno de alineación.

Después de introducir el contenido del objeto, lo que está relacionado con el bloqueo es obviamente el contenido almacenado en el encabezado del objeto:

  • Entre ellos, el bloqueo de peso pesado también se denomina bloqueo de objeto sincronizado, donde el puntero apunta a la dirección de inicio del objeto del monitor (también llamado monitor o bloqueo del monitor). Cada objeto tiene un monitor asociado, el monitor es implementado por ObjectMonitor e implementado en C ++.

  • Tenga en cuenta que también hay un bloqueo ligero, que es una mejora de la implementación subyacente de la palabra clave sincronizada después de jdk6.

2.3 Principio de sincronizado

Ya sabemos que sincronizado está relacionado con las instrucciones en el encabezado del objeto, que es lo que solíamos decir:

Java虚拟机可以支持方法级的同步和方法内部一段指令序列(代码块)的同步,这两种同步结构都是使用管程( Monitor,更常见的是直接将它称为“锁”) 来实现的。

Ahora hablamos del principio.

Debido a que los métodos de modificación sincronizados (incluidos los métodos ordinarios y estáticos) y los bloques de código modificados se implementan de manera ligeramente diferente:

1.método de modificación sincronizada

Probamos un método de sincronización:

public class Tues {
    public static int i ;
    public synchronized static void syncTask(){
        i++;
    }
}

Luego descompila el archivo de clase, puedes ver:

El ID del método:

  • ACC_PUBLIC Representa modificación pública

  • ACC_STATIC Significa método estático

  • ACC_SYNCHRONIZED Indica que el método es un método sincrónico.

En este momento, podemos comprender la descripción de la implementación subyacente del método de sincronización en "Comprensión profunda de la máquina virtual Java" de la siguiente manera:

La sincronización a nivel de método está implícita . No es necesario controlar mediante instrucciones de código de bytes, se implementa en operaciones de llamada y retorno de método. La máquina virtual puede saber si un método se declara como un método síncrono a partir del indicador de acceso ACC_SYNCHRONIZED en la estructura de la tabla de métodos en el grupo de constantes de métodos. (Lo mismo es cierto para los métodos estáticos)

  • Cuando se llama al método, la instrucción de llamada verificará si el indicador de acceso ACC_SYNCHRONIZED del método está establecido. Si está establecido, el subproceso de ejecución debe primero mantener exitosamente el monitor (Monitor) antes de ejecutar el método, y finalmente cuando el método se completa (independientemente de lo normal El monitor se libera cuando se completa o se completa anormalmente).

  • Durante la ejecución del método, el subproceso de ejecución retiene el monitor y ningún otro subproceso puede adquirir el mismo monitor.

  • Si se lanza una excepción durante la ejecución de un método sincronizado, y la excepción no se puede manejar dentro del método, el monitor retenido por el método sincronizado se liberará automáticamente cuando la excepción se lanza fuera del límite del método sincronizado.

2.bloque de código modificado sincronizado

Pruebe un fragmento de código de sincronización:

public class Tues {
   public int i;
   public void syncTask(){
       synchronized (this){
           i++;
       }
   }
}

Luego descompila el archivo de clase:

Se puede ver que en términos de instrucciones, hay más instrucciones para el funcionamiento del monitor, o la diferencia con el método de modificación anterior es usar instrucciones para operar el monitor (Monitor) explícitamente.

De la misma forma, en este momento podemos entender la descripción en "Una comprensión profunda de la máquina virtual Java" de la siguiente manera:

La situación de sincronizar una secuencia de conjunto de instrucciones . Hay dos instrucciones en el conjunto de instrucciones de la máquina virtual Java, monitorenter y monitorexit, para respaldar la semántica de la palabra clave sincronizada. (Las dos instrucciones de monitorenter y monitorexit están implementadas en lenguaje C) La implementación correcta de la palabra clave sincronizada requiere la cooperación y el apoyo del compilador Javac y la máquina virtual Java. La implementación de Monitor es básicamente código C ++, a través del funcionamiento de JNI (interfaz nativa de Java), programación interactiva con cpu directamente.

2.4 El problema de la sincronización temprana

En cuanto a la implementación específica de operar el monitor, no fuimos más allá, la idea de mantener monitores, contar, bloquear, etc. es similar a usar bloqueos directamente en java.

La implementación temprana de sincronizado se basa en el principio mencionado anteriormente, porque el bloqueo del monitor (monitor) es implementado por el bloqueo Mutex del sistema operativo subyacente , y el sistema operativo debe cambiar del modo de usuario a En el estado central , la transición entre este estado lleva un tiempo relativamente largo y el costo de tiempo es relativamente alto, por lo que la sincronización temprana es ineficaz.

Los gastos generales más específicos también involucran la relación entre los subprocesos de Java y los subprocesos del kernel del sistema operativo

Al hablar del contenido almacenado en el encabezado del objeto, también dejamos una pista, es decir, se agrega un candado ligero después de jdk6 para mejorar la implementación de sincronizados.

Según tengo entendido, esta mejora es: en el proceso desde el bloqueo hasta convertirse en el bloqueo pesado anterior, se implementan nuevos bloqueos con diferentes estados como una transición.

2.5 Varias cerraduras mejoradas

Bloqueo de deflexión -> Bloqueo de giro -> Bloqueo de peso ligero -> Bloqueo de peso pesado . En este orden, el peso de la cerradura aumenta secuencialmente.

  • Bloqueo de sesgo . Lo que quiere decir es que el bloqueo estará sesgado hacia el subproceso que lo obtenga primero, cuando este subproceso solicita el bloqueo nuevamente, no necesita realizar ninguna operación de sincronización, mejorando así el rendimiento. Luego, cuando esté en el modo de bloqueo de polarización, la estructura de Mark Word del cabezal del objeto cambiará a la estructura de bloqueo de polarización.

La investigación ha encontrado que en la mayoría de los casos, los bloqueos no solo están libres de competencia de múltiples subprocesos, sino que siempre son adquiridos por el mismo subproceso varias veces. Por lo tanto, para reducir el costo de adquirir bloqueos por el mismo subproceso, se introducen bloqueos sesgados. Entonces, obviamente, una vez que otro hilo intente adquirir este bloqueo, el modo sesgado terminará. Por otro lado, si varios subprocesos acceden a la mayoría de los bloqueos del programa, los bloqueos sesgados son redundantes.

  • Cerradura ligera . Cuando no se cumple la condición del bloqueo sesgado, es decir, cuando de hecho hay varios subprocesos que compiten por el mismo objeto de bloqueo al mismo tiempo, pero el número de simultaneidad no es grande, se prefiere el bloqueo ligero. Generalmente, cuando solo dos hilos compiten por la marca de bloqueo, se prefiere el bloqueo ligero. En este momento, la estructura de Mark Word del encabezado del objeto se convertirá en una estructura de bloqueo ligera.

Las cerraduras ligeras se comparan con las cerraduras pesadas tradicionales. Las cerraduras tradicionales utilizan el mutex del sistema operativo, mientras que las cerraduras ligeras se actualizan en función de las operaciones CAS de la máquina virtual. Intente comparar e intercambiar y decidir según la situación. ¿Quieres cambiar a un candado pesado? (Este proceso dinámico también es el proceso de bloqueo de giro)

  • Cerradura de peso pesado . La cerradura de peso pesado es la cerradura con la función de monitor completa descrita anteriormente .

  • Bloqueo giratorio . El bloqueo de giro es un bloqueo de transición, una transición de un bloqueo ligero a un bloqueo de peso pesado. Eso es CAS.

CAS, el nombre completo de Compare-And-Swap, es una instrucción atómica de la CPU. Su función es permitir que la CPU actualice atómicamente el valor de una determinada posición después de la comparación. El método de implementación se basa en la instrucción de ensamblaje de la plataforma de hardware, lo que significa que CAS se implementa por hardware Sí, JVM simplemente encapsula las llamadas de ensamblado y esas clases de AtomicInteger usan estas interfaces encapsuladas.

Nota: Los distintos bloqueos en Java son transparentes para el programador: cuando se crea un bloqueo, la JVM crea primero el bloqueo más ligero y, si no se cumplen las condiciones, el bloqueo se actualiza uno por uno. Estos cuatro bloqueos solo se pueden actualizar, no degradar.

2.6 Clasificación de otras cerraduras

Los bloqueos mencionados anteriormente se basan todos en la palabra clave sincronizada y el concepto de bloqueos involucrados en la implementación subyacente.También hay clasificaciones de bloqueos desde otras perspectivas:

Según las características de las cerraduras:

  1. Bloqueo pesimista : Un bloqueo exclusivo hará que todos los demás hilos que necesiten ser suspendidos, esperando que el hilo que sujeta el bloqueo libere el bloqueo, es decir, tiene una vista pesimista, pensando que el bloqueo pesimista piensa que la operación concurrente de los mismos datos debe ser Modificado. Por lo tanto, para operaciones concurrentes sobre los mismos datos, el bloqueo pesimista toma la forma de bloqueo. Por ejemplo, como se mencionó anteriormente, la implementación subyacente más tradicional de modificación sincronizada o bloqueo pesado. (Pero ahora, después de la actualización sincronizada, ya no es un bloqueo puramente pesimista)

  2. Bloqueo optimista : cada vez que no se bloquea, pero asumiendo que no hay conflicto, la operación se completa tentativamente, si falla por el conflicto, volverá a intentarlo hasta lograrlo. Por ejemplo, el funcionamiento de los bloqueos de giro CAS no está realmente bloqueado.

Clasificados según el orden de las cerraduras:

  1. Cerradura justa . Un bloqueo justo significa que varios subprocesos adquieren bloqueos en el orden en que se aplican para los bloqueos. En Java, puede pasar ReentrantLock este objeto de bloqueo y luego especificar si es justo

  2. Bloqueo injusto . Un bloqueo injusto significa que el orden en el que varios subprocesos adquieren bloqueos no está en el orden de aplicación de bloqueos. Es posible que el subproceso que se aplica más tarde obtenga el bloqueo primero que el subproceso que se aplica primero. Es imposible especificar la equidad o no sincronizar, es injusto.

Bloqueo exclusivo (también llamado bloqueo exclusivo) / bloqueo compartido:

  1. Un candado exclusivo también se llama candado exclusivo , lo que significa que el candado solo puede ser retenido por un hilo a la vez. Tanto ReentrantLock como Sychronized son bloqueos exclusivos.

  2. Bloqueo compartido : significa que varios subprocesos pueden mantener el bloqueo. Para ReentrantReadWriteLock, su bloqueo de lectura es un bloqueo compartido y su bloqueo de escritura es un bloqueo exclusivo. La naturaleza compartida de los bloqueos de lectura asegura que las lecturas simultáneas sean muy eficientes y que los procesos de lectura, escritura y escritura sean mutuamente excluyentes.

El bloqueo exclusivo / bloqueo compartido es un término amplio, el bloqueo de exclusión mutua / bloqueo de lectura y escritura es una implementación específica en Java.

Tres, bloquear en Java

Como mencionamos anteriormente, el bloqueo bajo la palabra clave sincronizada se implementa en el nivel de jvm. Más tarde, después de jdk 5, hay un bloqueo explícito en el paquete juc . El bloqueo está completamente escrito en Java. A nivel de Java, no tiene nada que ver con JVM. Logrado. Aunque Lock carece de la conveniencia de la adquisición y liberación implícitas de cerraduras (proporcionada por métodos o bloques sincronizados), tiene la capacidad de operación de adquisición y liberación de cerraduras, adquisición interrumpible de cerraduras y adquisición de tiempo de espera de múltiples claves sincronizadas. Funciones de sincronización que la palabra no tiene.

Lock es una interfaz. Las clases de implementación comunes son:

  • Bloqueo de reentrada ( ReentrantLock)

  • Leer bloqueo ( ReadLock)

  • Bloqueo de escritura ( WriteLock)

La implementación básicamente completa el control de acceso al subproceso agregando una subclase de sincronizador ( AbstractQueuedSynchronizer abreviado como  AQS).

Podemos mirar:

Cada bloqueo implementa la interfaz Lock, y luego puede abrir una clase arbitrariamente para encontrar la implementación en su interior. La operación de Lock se basa en la clase interna Sync, y Sync hereda la clase AbstractQueuedSynchronizer. Esta clase es una clase AQS muy importante.

En general, la relación entre estas clases es bastante complicada:

Pero el uso directo general sigue siendo muy simple, como un nuevo bloqueo, y luego bloquear y liberar el bloqueo antes y después de la operación requerida.

Lock lock = new ReentrantLock();
lock.lock();//获取锁的过程不要写在 try 中,因为如果获取锁时发生了异常,异常抛出的同时也会导致锁释放
try{

}finally{
    lock.unlock();//finally块中释放锁,目的是保证获取到锁之后最后一定能释放锁。
}

Hay 6 métodos definidos en la interfaz de bloqueo, y sus significados son los siguientes:

A continuación, echemos un vistazo a las clases de uso común paso a paso.

3.1 AbstractQueuedSynchronizer

Sincronizador de cola AbstractQueuedSynchronizer (en adelante, sincronizador o AQS) es un marco básico que se utiliza para crear bloqueos u otros componentes de sincronización. Utiliza una variable miembro int para indicar el estado de sincronización y completa el trabajo de puesta en cola de los subprocesos de adquisición de recursos a través de la cola FIFO incorporada .

El uso principal del sincronizador es la herencia . Las subclases administran el estado de sincronización heredando el sincronizador e implementando sus métodos abstractos. En la implementación del método abstracto, es inevitable cambiar el estado de sincronización. En este momento, es necesario utilizar el sincronizador proporcionado Tres métodos para operar, porque pueden garantizar que el cambio de estado sea seguro.

Los tres métodos son:

  1. protected final int getState(), // Obtiene el estado de sincronización actual

  2. protected final void setState(int newState), // Establecer el estado de sincronización actual

  3. protected final boolean compareAndSetState(int expect, int update), // Use CAS para establecer el estado actual, este método puede garantizar la atomicidad de la configuración del estado

Se recomienda que la subclase se defina como una clase interna estática de un componente de sincronización personalizado. El sincronizador en sí no implementa ninguna interfaz de sincronización. Solo define una serie de métodos de adquisición y liberación del estado de sincronización para que los utilice el componente de sincronización personalizado. El sincronizador puede admitir El estado de sincronización se puede obtener de forma exclusiva o compartida, de modo que se puedan implementar fácilmente diferentes tipos de componentes de sincronización (ReentrantLock, ReentrantReadWriteLock, CountDownLatch, etc.).

Tres tipos de métodos de plantilla definidos por AQS;

  1. Adquisición y liberación exclusivas del estado de sincronización

  2. Adquisición y liberación del estado de sincronización compartida

  3. Estado de sincronización y consulta del estado de los hilos en espera en la cola de sincronización

La cola FIFO incorporada del sincronizador, como puede ver en el código fuente, Node es un contenedor que contiene referencias de hilo y estado de hilo .

  • El acceso de cada hilo al sincronizador puede considerarse como un nodo en la cola.

  • El nodo es la base de la cola de sincronización, y el sincronizador tiene un nodo principal y un nodo final;

  • El hilo que no haya obtenido con éxito el estado de sincronización será el final del nodo para unirse a la cola.

  • Cuando el subproceso del primer nodo libera el estado de sincronización, el nodo siguiente se despertará y el nodo siguiente se establecerá como el primer nodo cuando el estado de sincronización se obtenga con éxito.

Debido a que hay muchos códigos fuente, no analizaré la implementación específica por ahora.

3.2 ReentrantLock

  • ReentrantLock es un bloqueo que admite la reentrada, lo que significa que el bloqueo puede admitir el bloqueo repetido de recursos por un hilo.

  • Además, el candado también admite elecciones justas e injustas al adquirir el candado.

ReentrantLock admite elecciones justas e injustas. El mecanismo de implementación interno es:

  1. El interno se basa en la  AQS realización de una clase principal pública justa e injusta  Sync (en el código, Sync es una clase interna que hereda AQS) para administrar el estado de sincronización;

  2. FairSync La herencia se  Sync utiliza para tratar cuestiones de equidad;

  3. NonfairSync La herencia se  Sync usa para tratar asuntos injustos.

3.3 ReentrantReadWriteLock

Al final del sincronizado anterior, se menciona la clasificación de otras dimensiones de cerraduras:

Bloqueo exclusivo (bloqueo exclusivo) / bloqueo compartido, el nivel de implementación específico corresponde al bloqueo de exclusión mutua / bloqueo de lectura y escritura en java .

  • ReentrantLock y sincronizado son bloqueos exclusivos;

  • ReentrantReadWriteLock
    mantiene un bloqueo de lectura y un bloqueo de escritura El bloqueo de lectura es un bloqueo compartido y el bloqueo de escritura es un bloqueo exclusivo.

Debido a la división del bloqueo de lectura y escritura, el bloqueo ReentrantReadWriteLock no implementa directamente la interfaz de bloqueo, su interno es así:

  • Basado en la  AQS realización de una clase principal pública justa e injusta  Sync , utilizada para administrar el estado de sincronización;

  • FairSync La herencia se  Sync utiliza para tratar cuestiones de equidad;

  • NonfairSync La herencia se  Sync usa para tratar asuntos injustos;

  • ReadLock Implementar  Lock interfaz, agregación interna  Sync;

  • WriteLock Implemente  Lock interfaces y agregue internamente  Sync.

Cuatro, un resumen y una comparación.

En este punto, sabemos que los objetos Java tienen un bloqueo asociado. Este bloqueo se llama bloqueo de monitor o bloqueo interno. Se usa a través de synchronized declaraciones de palabras clave  . De hecho, se implementa a nivel de JVM. La clase Monitor se usa hacia abajo. Las instrucciones de la próxima máquina virtual son tratar con la CPU, insertar barreras de memoria, etc.

Después de jdk 5 显式的锁, Lock se introdujeron varias clases de implementación con  interfaces como núcleo. Java las implementa completamente. Luego, las clases de implementación también se basan en AQS este sincronizador de cola. AQS protege las operaciones subyacentes, como la gestión del estado de sincronización, la puesta en  cola de subprocesos y la activación. Proporcione métodos de plantilla y agréguelos a la clase de implementación de Lock.

Aquí comparamos bloqueos implícitos y explícitos:

  1. El bloqueo implícito es básicamente inflexible, porque el bloque de código controlado por sincronizado no puede cruzar métodos, y el alcance de la modificación es muy estrecho; mientras que el bloqueo explícito en sí es un objeto, que puede dar pleno juego a la flexibilidad de la orientación a objetos, y puede estar en un método. Adquiera el candado y libérelo con otro método.

  2. Los bloqueos implícitos son simples y fáciles de usar y no causarán fugas de memoria, mientras que el proceso de bloqueos explícitos está completamente controlado por el programador, lo que fácilmente conduce a fugas de bloqueo;

  3. Los bloqueos implícitos son solo bloqueos injustos, los bloqueos explícitos apoyan bloqueos justos / injustos;

  4. Los bloqueos implícitos no pueden limitar el tiempo de espera y no pueden monitorear la información del bloqueo, los bloqueos de pantalla proporcionan suficientes métodos para completar funciones flexibles;

  5. En términos generales, usamos bloqueos implícitos por defecto y solo usamos bloqueos explícitos cuando necesitamos mostrar las características del bloqueo.

La comparación se completa  synchronized con  Lock dos cerraduras . Para el mecanismo de sincronización de subprocesos de Java, las otras dos cosas que se mencionan a menudo son  volatile palabras clave y  CAS operaciones y las clases atómicas correspondientes.

Entonces aquí hay otra mención:

  • volatile La palabra clave a menudo se llama ligero sincronizado, pero de hecho estos dos no son lo mismo en absoluto. Sabemos que sincronizado está bloqueado implícitamente por el monitor de nivel jvm. La palabra clave volátil es otro ángulo. JVM también adopta los medios correspondientes para garantizar:

    • Visibilidad de las variables modificadas por él: Después de que el hilo modifique la variable, debe volver a escribirse en la memoria principal inmediatamente;

    • Cuando el hilo lee la variable, debe leerla de la memoria principal, no de la caché;

    • Las operaciones en sus variables modificadas prohíben el reordenamiento de instrucciones.

  • CAS Es una instrucción de CPU y no pertenece al bloqueo. Completa provisionalmente la operación asumiendo que no hay conflicto. Si falla debido al conflicto, lo volverá a intentar hasta que tenga éxito. De hecho, rara vez usamos CAS directamente, pero java proporciona algunas clases de variables atómicas, que son varias clases Atomicxxx en el paquete juc. La implementación subyacente de estas clases usa directamente operaciones CAS para asegurar que se usen estos tipos de variables. Las operaciones son todas operaciones atómicas. Cuando se utilizan como variables compartidas, no hay ningún problema de seguridad de subprocesos.


     

Supongo que te gusta

Origin blog.csdn.net/bjmsb/article/details/108682242
Recomendado
Clasificación