En el código del programa C, podemos usar el bloqueo mutex proporcionado por el sistema operativo para lograr el acceso mutex a bloques sincrónicos y el bloqueo de hilos y trabajo de activación. Sin embargo, en Java, además de proporcionar LockAPI, también se proporcionan palabras clave sincronizadas a nivel de sintaxis para implementar primitivas de sincronización Mutex. Entonces, ¿cómo se implementa la clave sincronizada en el JVM?
1. Representación sincronizada de Bytecode:
Hay dos sintaxis sincronizadas incorporadas en el lenguaje Java: 1. Declaraciones sincronizadas; 2. Método sincronizado. Para declaraciones sincronizadas cuando el código fuente de Java se compila en ByTecode por Javac, MonitorEenter y las instrucciones de Bytecode de Monitorexit se insertarán respectivamente en las posiciones de entrada y salida del bloque de sincronización. El método sincronizado se traducirá al método ordinario de llamadas y instrucciones de devolución, tales como: Instrucciones de InvokeVirtual y Areturn. No hay instrucción especial en el nivel de bytecode VM para implementar el método modificado sincronizado. En su lugar, la posición del indicador sincronizada 1 en el campo Access_flags del método del método se coloca en la tabla de métodos del archivo de clase, lo que indica que el método es un método sincronizado y utiliza el objeto que llama al método o la clase que pertenece al método para representar KLASS como un objeto de bloqueo.
2. Optimización de bloqueos en JVM:
En pocas palabras, MonitorEnenter y Monitorexit bytecode en JVM confían en Mutexlock del sistema operativo subyacente para implementarlo. Sin embargo, dado que el uso de Mutexlock requiere suspender el hilo actual y cambiar de estado de usuario a estado del núcleo para ejecutar, este cambio es muy costoso; Sin embargo, en la mayoría de los casos en realidad, el método de sincronización se ejecuta en un entorno de un solo hilo (entorno de competencia sin bloqueo). Si se llama a Mutexlock cada vez, afectará seriamente el rendimiento del programa. Sin embargo, en JDK1.6, se han introducido muchas optimizaciones en la implementación de cerraduras, como el engrosamiento de bloqueos, la eliminación de bloqueos, el bloqueo liviano, el bloqueo sesgado, el hilado adaptativo y otras tecnologías para reducir la sobrecarga de la operación de bloqueo.
LockCoarsening: es decir, reducir las operaciones innecesarias de bloqueo y bloqueo, expandir múltiples cerraduras continuas en una cerradura con un rango más grande.
Eliminación de bloqueo: a través del análisis de escape por el compilador JIT de tiempo de ejecución, se elimina cierta protección de bloqueo. Algunos datos que no son compartidos por otros hilos fuera del bloque de sincronización actual. A través del análisis de escape, el espacio de objetos se puede asignar en la pila local de subprocesos (al mismo tiempo, también puede reducir la sobrecarga de la recolección de basura en el montón).
Ligero de peso: la implementación de este bloqueo se basa en el supuesto de que en casos reales, la mayor parte del código de sincronización en nuestro programa generalmente se encuentra en un estado de competencia sin bloqueo (es decir, un entorno de ejecución de un solo hilo). En el caso de la competencia sin bloqueo, puede evitar llamar por completo a los mutexes de peso pesado a nivel del sistema operativo. En cambio, en Monitorenter y Monitorexit, solo necesita confiar en una instrucción atómica CAS para completar la adquisición y la liberación del bloqueo. Cuando hay una competencia de bloqueo, el hilo que no ejecuta las instrucciones de CAS llamará al sistema operativo Mutex para ingresar al estado de bloqueo y despertar cuando se libera el bloqueo (los pasos de procesamiento específicos se analizan en detalle a continuación).
Besedlocking: es para evitar ejecutar instrucciones atómicas de CAS innecesarias durante la adquisición de bloqueos en el caso de la competencia sin bloqueo, porque aunque las instrucciones atómicas de CAS son relativamente pequeñas en el costo en comparación con los bloqueos de peso pesado, todavía tienen retrasos locales muy considerables (ver este artículo).
Spinning adaptativo: cuando un hilo no realiza la operación de CAS durante la adquisición de un bloqueo liviano, ingresará una espera ocupada antes de ingresar al bloqueo de peso pesado del sistema operativo (mutexsemaphore) asociado con el monitor y luego lo intentará nuevamente. Si todavía falla después de un cierto número de intentos, se llama al semáforo asociado con el monitor (es decir, el bloqueo mutex) para ingresar al estado de bloqueo.
3. ObjecTheader:
Al crear un objeto en el JVM, se agregarán encabezados de objetos de dos palabras frente al objeto. Una palabra en la máquina de 32 bits es de 32 bits. Diferentes contenidos se almacenan en MarkWorld de acuerdo con diferentes bits de estado. Como se muestra en la figura anterior, en un bloqueo liviano, Markword se divide en dos partes. Al principio, Lockword está configurado como hashcode, y los tres bits más bajos representan el estado donde se encuentra Lockword. El estado inicial es 001, lo que significa el estado sin bloqueo. KLASSPTR señala la dirección representada por el objeto cuya clase Bytecode está dentro de la máquina virtual. Los campos representan campos de instancia de objetos continuos.
4. Monitorrecord:
Monitorrecord es una estructura de datos privada de hilos. Cada hilo tiene una lista de Monitorrecords disponibles y una lista global disponible. Entonces, ¿cuáles son los usos de estos monitorrecords? Cada objeto bloqueado se asociará con un Monitorrecord (la palabra de bloqueo en el encabezado del objeto apunta a la dirección de inicio de Monitorrecord. Dado que esta dirección está alineada 8 byte, los tres bits más bajos de Lockword se pueden usar como bits de estado). Al mismo tiempo, hay un campo del propietario en Monitorrecord para almacenar el identificador único del hilo que posee el bloqueo, lo que indica que el bloqueo está ocupado por este hilo. La siguiente figura muestra la estructura interna del monitorrecord:
Propietario: NULL Al principio significa que ningún hilo actualmente posee el registro del monitor. Cuando el hilo posee con éxito el bloqueo, guarda la identidad única del hilo, y cuando se libera el bloqueo, está configurado para nulo;
Entrada: Asociar un sistema mutex (semáforo) para bloquear todos los hilos que no pueden bloquear el registro del monitor.
RCTHIS: representa el número de todos los hilos bloqueados o esperando en el registro del monitor.
Nido: se usa para implementar el conteo de cerraduras de reingreso.
Hashcode: guarda el valor de hashcode copiado del encabezado del objeto (también puede contener la edad de GC).
Candidato: se usa para evitar un bloqueo innecesario o esperar que los hilos se despierten, porque solo un hilo puede poseer con éxito el bloqueo cada vez. Si el hilo anterior que libera el bloqueo despierta todos los hilos de bloqueo o espera cada vez, causará un cambio de contexto innecesario (desde el bloqueo hasta listo y luego el bloqueo nuevamente debido a la falla del bloqueo de la competencia) y, por lo tanto, conducirá a una degradación severa del rendimiento. El candidato tiene solo dos valores posibles: 0 significa que no hay un hilo que deba despertar hasta 1 medios para despertar un hilo sucesor para competir por el bloqueo.
5. Implementación específica de bloqueos livianos:
Un hilo puede bloquear un objeto de dos maneras: 1. Obtenga el bloqueo del objeto expandiendo un objeto en un estado sin bloqueo (Bit de estado 001); 2. El objeto ya está en un estado ampliado (bit de estado 00), pero el campo del propietario del registro del monitor apuntado por Lockword es nulo, por lo que puede intentar establecer directamente al propietario en su propia identidad a través de la instrucción atómica de CAS para obtener el bloqueo.
El proceso general de obtener un bloqueo (monitor) es el siguiente:
(1) Cuando el objeto está en un estado sin bloqueo (el valor de la palabra de registro es hashcode, el bit de estado es 001), el hilo primero obtiene un registro de monitor gratuito de su lista de registros de monitor disponible. El nido inicial y los valores del propietario están preestablecidos a 1 y la propia identificación del hilo respectivamente. Una vez que el registro del monitor está listo, instalamos la dirección de inicio del registro del monitor en el campo Lockword del encabezado del objeto a través de la instrucción atómica de CAS para expandirse (el texto original se infla. Creo que la razón por la que se llama inflate principalmente porque el objeto se expande después de que se expande; para la eficiencia espacial, el monitor se usa para expandir el tamaño del objeto; la estructura de registro se extrae del encabezado del objeto y solo se adjunta al objeto que se necesita. A este documento.
(2) El objeto se ha ampliado y el hilo guardado en el propietario se identifica como el hilo que adquiere el bloqueo en sí. Este es el caso de las cerraduras reentrantes. Solo necesitas agregar 1 al nido. No se requiere operación atómica y muy eficiente.
(3) El objeto se ha ampliado pero el valor del propietario es nulo. Este estado ocurre cuando un bloqueo de hilo o espera en una cerradura está bloqueado al mismo tiempo, el propietario anterior de la cerradura acaba de lanzar la cerradura. En este momento, múltiples hilos intentan establecer al propietario en su propia identidad a través de la instrucción atómica de CAS para obtener el bloqueo en un estado de competencia multiproceso. El hilo que no compite ingresará la ruta de ejecución del cuarto caso (4).
(4) El objeto está en un estado ampliado y el propietario no es nulo (bloqueado). Gira una cierta cantidad de veces antes de llamar al mutex de peso pesado del sistema operativo. Cuando se alcanza un cierto número de veces, si el bloqueo aún no se obtiene con éxito, es hora de comenzar a ingresar al estado de bloqueo. Primero, agregue el valor de RFTHIS atómicamente por 1. Dado que otros hilos pueden destruir la relación entre el objeto y el registro de monitor durante la adición de 1, es necesario realizar otra comparación después de agregar 1 para garantizar que el valor de Lockword no haya cambiado. Cuando se encuentra que se ha cambiado, el proceso del monitor se debe repetir. Al mismo tiempo, se observa nuevamente si el propietario es nulo. Si es así, llamará a CAS para participar en el bloqueo de la competencia. Si la competencia de bloqueo falla, entrará en un estado de bloqueo.
El proceso general de liberar el bloqueo (monitorexit) es el siguiente:
(1) Primero verifique si el objeto está en un estado ampliado y el hilo es el propietario del bloqueo. Si se encuentra que está mal, se lanzará una excepción;
(2) Verifique si el campo de nido es mayor que 1. Si es mayor que 1, simplemente reduzca el nido en 1 y continúe teniendo la cerradura. Si es igual a 1, ingrese el paso (3);
(3) Verifique si RFTHIS es mayor que 0, configure el propietario en Null y despierta un hilo de bloqueo o espera para intentar adquirir el bloqueo nuevamente. Si es igual a 0, ingresará el paso (4)
(4) Desinfle el objeto, suelte el bloqueo reemplazando la palabra de bloqueo del objeto al valor de hashcode original para liberar el bloqueo y volver a colocar el registro del monitor en el hilo.
Resumir
Referencia: " Entendencia en profundidad de las características avanzadas y las mejores prácticas de Java Virtual Machine JVM (Zhou Zhiming) "
Lo anterior es todo el contenido de este artículo sobre análisis de problemas sincronizados e de implementación de los detalles de JVM. Espero que sea útil para todos. Si hay alguna deficiencia, deje un mensaje para señalarlo. ¡Gracias amigos por su apoyo para este sitio!