Comprensión profunda de los principios subyacentes de la sincronización.

prefacio

Si un recurso es compartido por varios subprocesos, para evitar la confusión de datos de recursos debido a la preferencia de recursos, necesitamos sincronizar los subprocesos, entonces sincronizado es la palabra clave para lograr la sincronización de subprocesos, que se puede decir que es una parte esencial en el control de concurrencia. Hoy, echemos un vistazo al uso y los principios subyacentes de sincronizado.

En primer lugar, las características de sincronización

1.1 Atomicidad

La llamada atomicidad se refiere a una operación o múltiples operaciones, o bien se ejecutan todas y el proceso de ejecución no será interrumpido por ningún factor, o bien no se ejecuta ninguna.

En Java, las operaciones de lectura y asignación a variables de tipos de datos primitivos son operaciones atómicas, es decir, estas operaciones no son interrumpibles, se ejecuten o no. Sin embargo, los caracteres de operación como i++, i+=1etc. no son atómicos. Se dividen en varios pasos de lectura, cálculo y asignación. Es posible que el valor original se haya asignado antes de completar estos pasos, por lo que los datos escritos por la última asignación están sucios. .datos, no se puede garantizar la atomicidad. Todas las operaciones de una clase u objeto modificado por sincronizado son atómicas, porque el bloqueo de la clase u objeto debe obtenerse antes de realizar la operación, y no puede liberarse hasta que se complete la ejecución, y el proceso en el medio no puede ser interrumpido (salvo stop()métodos que hayan sido abandonados), es decir, se garantiza la atomicidad.

¡Darse cuenta! Sincronizado y volátil a menudo se comparan La mayor diferencia entre las dos características es la atomicidad, y volátil no tiene atomicidad.

1.2 Visibilidad

Visibilidad significa que cuando varios subprocesos acceden a un recurso, la información de estado y valor del recurso es visible para otros subprocesos.

Tanto sincronizado como volátil tienen visibilidad. Cuando sincronizado bloquea una clase u objeto, si un subproceso desea acceder a la clase u objeto, primero debe obtener su bloqueo, y el estado del bloqueo es visible para cualquier otro subproceso, y antes de liberar el lock, la modificación de la variable se actualizará en la memoria principal para garantizar la visibilidad de la variable de recurso. Si un subproceso ocupa el bloqueo, otros subprocesos deben esperar en el grupo de bloqueo para que se libere el bloqueo.

La implementación de volatile es similar. La variable modificada por volatile actualizará la memoria principal inmediatamente siempre que el valor deba modificarse. La memoria principal es compartida y visible para todos los subprocesos, por lo que se garantiza que las variables leídas por otros subprocesos siempre estén el último valor, garantizando la visibilidad.

1.3 Orden

Valor ordenado El orden en que se ejecutan los programas se ejecuta secuencialmente en el código.

Tanto los sincronizados como los volátiles están ordenados.Java permite que el compilador y el procesador reorganicen las instrucciones, pero la reorganización de las instrucciones no afecta el orden de un solo subproceso, afecta el orden de ejecución concurrente de múltiples subprocesos. Synchronized asegura que solo un subproceso acceda al bloque de código sincronizado en cada momento, lo que también determina que el subproceso ejecute el bloque de código sincronizado en secuencia, asegurando el orden.

1.4 Reentrada

Tanto sincronizado como ReentrantLock son bloqueos reentrantes. Cuando un subproceso intenta operar un recurso crítico de un bloqueo de objeto retenido por otro subproceso, estará en un estado de bloqueo, pero cuando un subproceso solicita nuevamente un recurso crítico que retiene un bloqueo de objeto por sí mismo, esta situación es un bloqueo reentrante. En términos sencillos, significa que un subproceso que posee el bloqueo aún puede solicitar el bloqueo repetidamente.

En segundo lugar, el uso de sincronización

Synchronized puede modificar métodos estáticos, funciones miembro y definir directamente bloques de código, pero en el análisis final, solo bloquea dos tipos de recursos: uno es un objeto y el otro es una clase .

Eche un vistazo al siguiente código primero (los principiantes no deben marearse cuando lo vean y explicarlo lentamente más adelante):

imagen

En primer lugar, sabemos que los staticmétodos estáticos modificados y las propiedades estáticas están todos clasificados, y se puede acceder a todos los objetos de instancia de la clase. Sin embargo, las propiedades de los miembros ordinarios y los métodos de los miembros son propiedad del objeto instanciado y solo se puede acceder a ellos después de la instanciación, por lo que los métodos estáticos no pueden acceder a las propiedades no estáticas. Una vez que hayamos aclarado qué propiedades y métodos son propiedad, podemos entender a quién se agregan los bloqueos sincronizados anteriores.

En primer lugar, el método agregado por el primer sincronizado es add1()que el método no se staticmodifica, es decir, el método es propiedad del objeto instanciado, luego se agrega este bloqueo al objeto instanciado de la clase Test1.

Luego está el add2()método, que es un método estático y es propiedad de la clase Test1, por lo que este bloqueo se agrega a la clase Test1.

Finalmente, method()hay dos bloques de código sincronizados en el método. El primer bloque de código está bloqueado Test1.classy se sabe literalmente que el bloqueo se agrega a la clase Test1, y el siguiente bloqueo es instanceque esta instancia es una instanciación de la clase Test1. El objeto, por supuesto, el bloqueo en él es instanciar el objeto para la instancia.

Debería ser fácil entender el uso de sincronizado al averiguar para quién son estos bloqueos, solo recuerde que para ingresar a un método sincronizado o bloque sincronizado, primero debe adquirir el bloqueo correspondiente. Luego voy a enumerar un código que es muy fácil de entender a continuación para ver si realmente entiendes la explicación anterior.

imagen

El significado simple de lo anterior es usar dos subprocesos para agregar 1 millón de veces a i respectivamente, el resultado teórico debería ser 2 millones, y también agregué sincronizado para bloquear el método de agregar para garantizar la seguridad de subprocesos. ¡Pero! ! ! No importa cuántas veces lo ejecute, es menos de 2 millones, ¿por qué?

El motivo es que la función de bloqueo sincronizado es un método de miembro común, por lo que el bloqueo se agrega al objeto, pero se crean dos nuevas instancias Test2 cuando se crea el subproceso, lo que significa que el bloqueo se agrega a estas dos instancias. no logra el efecto de sincronización , por lo que se produce el error. En cuanto a por qué es menos de 2 millones, i++el proceso para comprenderlo es claro, lea: Hable sobre las operaciones CAS en Java en detalle

3. Implementación de bloqueo sincronizado

Hay dos formas de bloqueo sincronizado, una es bloquear el método y la otra es construir un bloque de código sincronizado. Sus implementaciones subyacentes son en realidad las mismas. El bloqueo se adquiere antes de ingresar el código de sincronización, el contador del bloqueo es +1 después de que se adquiere el bloqueo, el contador del bloqueo es -1 después de que el código de sincronización ejecuta el bloqueo, y si la adquisición falla, se bloqueará y esperará a que se libere el bloqueo. Es solo que son diferentes en la forma de identificación de bloque síncrono, que se puede mostrar desde el archivo de código de bytes de clase, uno es a través del indicador de banderas de método, y el otro es la operación de las instrucciones monitorenter y monitorexit.

3.1 Método de sincronización

En primer lugar, veamos cómo bloquear el método. Definimos un nuevo método de sincronización y luego lo descompilamos para ver su código de bytes:

imagen

imagen

Se puede ver que hay una bandera adicional en las banderas del método add, ACC_SYNCHRONIZEDesta bandera se usa para decirle a la JVM que este es un método sincrónico, antes de ingresar al método, se adquiere el bloqueo correspondiente, el contador del bloqueo es incrementa en 1, y el contador es -1 después de que finaliza el método. Se bloquea si la adquisición falla hasta que se libera el bloqueo.

Los amigos que no entiendan las instrucciones del código de bytes pueden leer primero los siguientes dos artículos para comprender la estructura de la clase:

3.2 Bloques de código sincronizados

Definimos un nuevo bloque de código de sincronización, compilamos el código de bytes de la clase y luego encontramos el bloque de instrucciones donde se encuentra el método del método. Puede ver claramente el proceso de bloqueo y liberación del bloqueo. Las capturas de pantalla son las siguientes:

imagen

imagen

Desde el bloque de código de sincronización descompilado, podemos ver que la instrucción monitorenter ingresa al bloque de sincronización, y luego monitorexit libera el bloqueo. Antes de ejecutar monitorenter, debe intentar adquirir el bloqueo. Si el objeto no está bloqueado o el el subproceso actual ya posee el bloqueo del objeto, luego incremente el contador de bloqueo en 1. Cuando se ejecuta la instrucción monitorexit, el contador de bloqueo también se reduce en 1. Cuando falla la adquisición del bloqueo, se bloqueará, esperando a que se libere el bloqueo.

Pero, ¿por qué hay dos salidas de monitor? De hecho, la segunda salida del monitor es para manejar excepciones. Mire cuidadosamente el código de bytes descompilado. En circunstancias normales, la primera salida del monitor ejecutará la gotoinstrucción, y la instrucción pasa a la línea 23 return, lo que significa que, en circunstancias normales, solo ejecutará la primera salida del monitor. para soltar la cerradura y volver. Y si ocurre una excepción durante la ejecución, entra en juego la segunda salida del monitor, que el compilador genera automáticamente, maneja la excepción cuando ocurre y libera el bloqueo.

Cuarto, la implementación subyacente de bloqueos sincronizados

Antes de comprender el principio de la implementación de bloqueo, entendamos el encabezado del objeto y el monitor de Java. En la JVM, los objetos existen en tres partes: el encabezado del objeto, los datos de la instancia y su relleno.

imagen

Los datos de la instancia y su llenado no tienen nada que ver con la sincronización, así que hablemos brevemente sobre esto aquí (lea "Comprensión detallada de la máquina virtual Java", los lectores pueden leer los capítulos relevantes del libro con atención). Los datos de instancia almacenan la información de datos de atributos de la clase, incluida la información de atributos de la clase principal. Si la parte de la instancia de la matriz también incluye la longitud de la matriz, esta parte de la memoria se alinea en 4 bytes; no es necesario llenarlo , porque la máquina virtual requiere que el objeto se inicie.La dirección de inicio debe ser un múltiplo entero de 8 bytes, y el relleno de alineación es solo para alinear los bytes.

El encabezado del objeto es el punto clave al que debemos prestar atención. Es la base para sincronizar para implementar bloqueos, porque la aplicación sincronizada para bloqueos, bloqueos y liberación de bloqueos están todos relacionados con el encabezado del objeto. La estructura principal del encabezado del objeto se compone de yMark Word , que almacenan el código hash del objeto, la información de bloqueo, la edad de generación o el indicador GC y otra información Son los metadatos de clase del objeto al que apunta el puntero de tipo, y la JVM utiliza el puntero para determinar de qué clase es una instancia el objeto .Class Metadata AddressMark WordClass Metadata Address

Los bloqueos también se dividen en diferentes estados. Antes de JDK6, solo había dos estados: sin bloqueos y bloqueos (bloqueos pesados). Después de JDK6, se optimizó la sincronización y se agregaron dos nuevos estados. Hay cuatro estados en total: estados sin bloqueo , bloqueo sesgado, bloqueo ligero, bloqueo pesado , del cual ningún bloqueo es un estado. El tipo y el estado del bloqueo se registran en el encabezado del objeto , y la JVM necesita leer los datos del Mark Wordobjeto en el proceso de solicitar un bloqueo y actualizarlo .Mark Word

Cada bloqueo corresponde a un objeto de monitor, que es implementado por ObjectMonitor (implementación de C++) en la máquina virtual HotSpot. Cada objeto tiene un monitor asociado, y la relación entre el objeto y su monitor se puede implementar de varias maneras, por ejemplo, el monitor se puede crear y destruir junto con el objeto o se puede generar automáticamente cuando el subproceso intenta adquirir el objeto. lock, pero cuando un monitor está sostenido por un hilo, está bloqueado.

ObjectMonitor() {
    
    
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

Este párrafo está tomado de: https://blog.csdn.net/javazejian/article/details/72828483

Hay dos colas _WaitSet y _EntryList en ObjectMonitor, que se utilizan para guardar una lista de objetos ObjectWaiter (cada subproceso que espera un bloqueo será encapsulado por un objeto ObjectWaiter), _owner apunta al subproceso que contiene el objeto ObjectMonitor, cuando varios subprocesos acceden una pieza de código de sincronización al mismo tiempo, primero ingresará a la colección _EntryList. Cuando el subproceso obtenga el monitor del objeto, ingresará al área _Owner y establecerá la variable de propietario en el monitor en el subproceso actual. tiempo, el conteo del contador en el monitor se incrementa en 1. Si el subproceso llama al método wait(), se liberará el monitor retenido actualmente, la variable del propietario se restaurará a nulo, el conteo se entra en la colección WaitSet y espera a que lo despierten. Si el hilo actual termina de ejecutarse, también liberará el monitor (bloqueo) y restablecerá el valor de la variable, para que otros hilos puedan entrar y adquirir el monitor (bloqueo).

El objeto monitor existe en el encabezado del objeto de cada objeto Java (apuntando al puntero almacenado), y el bloqueo sincronizado adquiere el bloqueo de esta manera, por lo que cualquier objeto en Java puede usarse como bloqueo y también notificar/notificar a todos. / La razón por la que existen métodos como wait en el objeto de nivel superior Object (se analizará más adelante en este punto)

Cinco, optimización JVM de sincronización

Se puede ver en las versiones recientes de jdk que el equipo de desarrollo de Java ha estado optimizando la sincronización. La mayor optimización es que en jdk6, se agregaron dos nuevos estados de bloqueo a través de la eliminación de bloqueo, el engrosamiento de bloqueo y el giro. Los bloqueos y otros métodos usan varios escenarios. , lo que mejora en gran medida el rendimiento sincronizado.

5.1 Bloquear hinchazón

Como se mencionó anteriormente, el bloqueo tiene cuatro estados, y se expandirá y actualizará de acuerdo con la situación real. La dirección de expansión es: sin bloqueo -> bloqueo parcial -> bloqueo ligero -> bloqueo pesado , y la dirección de expansión es irreversible.

5.1.1 Bloqueo de polarización

Resume su función en una frase: reducir el costo de obtener mechones para naranjas unificadas . En la mayoría de los casos, no hay competencia de subprocesos múltiples para los bloqueos, y siempre se obtiene el mismo naranja varias veces, entonces es un bloqueo sesgado.

Idea principal:

Si un subproceso adquiere el bloqueo, el bloqueo entra en el modo sesgado, y Mark Wordla estructura en este momento se convierte en la estructura de bloqueo sesgado.Cuando el subproceso solicita el bloqueo nuevamente, no necesita realizar ninguna operación de sincronización, es decir, el proceso de adquirir el bloqueo solo necesita verificar Mark Wordel bit de indicador de bloqueo es un bloqueo sesgado y Mark Wordse puede usar el ThreadID igual al ID del subproceso actual , lo que ahorra muchas operaciones relacionadas con la aplicación de bloqueo.

5.1.2 Candado ligero

Los bloqueos ligeros se actualizan a partir de bloqueos sesgados. Cuando se aplica un segundo subproceso para el mismo objeto de bloqueo, los bloqueos sesgados se actualizarán a bloqueos ligeros inmediatamente. Tenga en cuenta que el segundo subproceso aquí solo se aplica a un bloqueo, no hay dos subprocesos que compitan por bloqueos al mismo tiempo, y los bloques sincronizados se pueden ejecutar alternativamente en tándem.

5.1.3 Bloqueo pesado

Los candados pesados ​​se actualizan a partir de candados livianos. Cuando varios subprocesos compiten por los candados al mismo tiempo , los candados se actualizarán a candados pesados ​​y la sobrecarga de solicitar candados aumentará.

Los bloqueos pesados ​​​​generalmente se utilizan en la búsqueda del rendimiento, y el bloque de sincronización o el método de sincronización tarda mucho tiempo en ejecutarse.

5.2 Eliminación de bloqueo

La eliminación de bloqueos es otro tipo de optimización de bloqueos para máquinas virtuales. Esta optimización es más exhaustiva. Durante la compilación JIT, se analiza el contexto en ejecución para eliminar los bloqueos que no pueden competir . Por ejemplo, la eficiencia de ejecución de method1 y method2 en el siguiente código es la misma, porque el objeto lock es una variable privada y no hay competencia de ingresos.

imagen

5.3 Engrosamiento de bloqueo

El engrosamiento de bloqueo es la optimización de la máquina virtual para otra situación extrema. Al expandir el alcance del bloqueo, evita bloquear y liberar repetidamente el bloqueo . Por ejemplo, el siguiente método 3 tiene la misma eficiencia de ejecución que el método 4 después de la optimización del engrosamiento de bloqueo.

imagen

5.4 Spinlocks y Spinlocks adaptativos

Después de que falla el bloqueo ligero, la máquina virtual realizará un método de optimización llamado bloqueo giratorio para evitar que el subproceso se suspenda realmente en el nivel del sistema operativo.

Spinlocks : en muchos casos, el estado bloqueado de los datos compartidos es de corta duración y no vale la pena cambiar los hilos, dejando que los hilos realicen un bucle esperando que se libere el bloqueo sin renunciar a la CPU. Si se obtiene el bloqueo, se ingresa con éxito a la sección crítica. Si aún no se puede adquirir el bloqueo, suspenderá el subproceso en el nivel del sistema operativo, que es como se optimizan los spinlocks. Pero también tiene desventajas: si el bloqueo está ocupado por otros subprocesos durante mucho tiempo y la CPU no se libera, generará una gran sobrecarga de rendimiento.

Bloqueo de giro adaptativo : esto es equivalente a una optimización adicional del método de optimización de bloqueo de giro anterior. El número de giros ya no es fijo, y el número de giros está determinado por el tiempo de giro anterior en el mismo bloqueo y el bloqueo. El estado del propietario se determina, lo que resuelve las deficiencias de las cerraduras giratorias.

Epílogo

La palabra clave sincronizada es una parte indispensable de la programación concurrente. Personalmente, creo que una verdadera comprensión de su funcionamiento interno puede ser de gran ayuda para el desarrollo habitual. ¡Espero que este artículo pueda ayudarte!

Supongo que te gusta

Origin blog.csdn.net/qq_43842093/article/details/124463485
Recomendado
Clasificación