El principio de implementación subyacente del mecanismo de concurrencia concurrente de Java

Prefacio

El código finalmente debe convertirse en instrucciones de ensamblaje para ser ejecutadas en la CPU El mecanismo de concurrencia utilizado en Java depende de la implementación de la JVM y de las instrucciones de la CPU.
Este artículo profundizará en la parte inferior para comprender el principio de implementación subyacente del mecanismo de concurrencia. Presentará:

  1. El principio de realización de volátiles
  2. Principio de realización final
  3. El principio de realización de sincronizados
  4. El principio de realización del funcionamiento atómico.

1. El principio de definición y realización de volátiles

  • Volatile define el
    lenguaje Java para permitir que los subprocesos accedan a variables compartidas. Para garantizar que las variables compartidas se puedan actualizar de forma precisa y coherente, los subprocesos deben garantizar que esta variable se obtenga por separado a través de un bloqueo exclusivo.
    Si una variable se declara como volátil, el modelo de memoria de subprocesos de Java garantizará que el valor de esta variable sea coherente en todos los subprocesos. Garantiza la visibilidad de las variables compartidas insertando barreras de memoria en el desarrollo multiprocesador y prohíbe el reordenamiento de instrucciones.

  • Definición de términos de CPU y tipos de barrera de memoria
    Definición de término de CPU
    Tipo de barrera de memoria

  • ¿Cómo garantiza volatile la visibilidad?
    Al observar las instrucciones de ensamblaje generadas, se encuentra que al escribir en volatile:

    1. La instrucción de prefijo de bloqueo escribe los datos de la línea de caché del procesador actual en la memoria del sistema
      • Para mejorar la velocidad de procesamiento, el procesador no se comunica directamente con la memoria, sino que primero lee los datos en la memoria del sistema en la caché interna (memoria de trabajo del hilo) antes de realizar la operación, pero no escribirá inmediatamente en la memoria principal. memoria una vez completada la operación.
      • Si se trata de una operación de escritura en una variable volátil, la JVM enviará una instrucción de prefijo de bloqueo al procesador para escribir los datos de la línea de caché donde se encuentra la variable en la memoria del sistema. Pero las cachés de otros procesadores aún son antiguas y habrá problemas al realizar operaciones de cálculo.
    2. Escribir la caché de un procesador en la memoria invalidará los datos almacenados en caché en esa dirección de memoria en otras CPU
      • Por lo tanto, para asegurar que las cachés de cada procesador sean consistentes, se implementará un protocolo de coherencia de caché.
        Cada procesador verifica si el valor de su caché está caducado olfateando los datos transmitidos en el bus. Cuando el procesador encuentra que la dirección de memoria correspondiente a su línea de caché ha sido modificada, establecerá la línea de caché del procesador actual en un valor no válido. Cuando el procesador modifica estos datos, volverá a leer los datos de la memoria principal a la caché del procesador.
  • El principio de volátil:
    en cada inserte una StoreStorebarrera de escritura prevolátil
    en cada operación de escritura volátil para insertar una StoreLoadbarrera
    después de cada operación de lectura volátil para insertar una LoadLoadbarrera
    inserte una operación de lectura después de cada LoadStorebarrera volátil
    que se basa en un JMM conservador La barrera de la memoria estrategia de inserción de la estrategia, en la implementación específica, siempre que no se cambie la semántica de la memoria de escritura-lectura volátil, el compilador puede omitir barreras innecesarias de acuerdo con la situación específica.

  • Volatile solo garantiza la visibilidad y el orden de concurrencia, pero no garantiza la atomicidad, tenga cuidado al usarlo.

2. Semántica de memoria de campos finales

  • Para el dominio final, el compilador y el procesador deben cumplir con dos reglas de reordenamiento:

    1. Escriba las reglas finales de reordenación del dominio: escriba en un dominio final en el constructor y luego asigne la referencia del objeto construido a una variable de referencia; las dos operaciones no se pueden reordenar.
    2. Reglas de reordenamiento para leer el dominio final: la primera vez que lee una referencia a un objeto que contiene el dominio final, y la lectura posterior de este dominio final por primera vez, las dos operaciones no se pueden reordenar.
    3. Para tipos de referencia: escribir en el campo miembro de un objeto de referencia final en el constructor y, posteriormente, asignar la referencia del objeto construido a una variable de referencia fuera del constructor, estas dos operaciones no se pueden reordenar (es decir, la primera Inicializar el miembro variable de la variable de referencia final).
  • Escribir las reglas de reordenación para los campos finales puede garantizar que el campo final del objeto se haya inicializado correctamente antes de que la referencia del objeto sea visible para cualquier hilo, y el campo ordinario no tiene esta garantía.

  • Las reglas de reordenación para leer el dominio final pueden garantizar que: antes de leer el dominio final de un objeto, se debe leer primero la referencia del objeto que contiene el dominio final.

  • Siempre que el objeto esté construido correctamente, es decir, la referencia al objeto construido no se escape en el constructor, entonces no hay necesidad de usar la sincronización para asegurar que cualquier hilo pueda ver el valor del campo final inicializado en el constructor. .

  • Principio de implementación final: las
    reglas de reordenación para escribir el campo final requieren que el compilador inserte una StoreStorebarrera después de que se haya escrito el campo final y antes de que el constructor regrese ; las
    reglas de reordenación para leer el campo final requieren que el compilador inserte una LoadLoadbarrera antes de la operación de leyendo el campo final .

3. El principio de realización de sincronizados

Esta sección presenta la optimización de bloqueos sincronizados, sesgados y bloqueos ligeros introducidos en Java 6 con el fin de reducir el consumo de rendimiento de la adquisición y liberación de bloqueos, así como la estructura de almacenamiento y el proceso de actualización de bloqueos.

  • Cuando un hilo intenta acceder a un bloque de código sincronizado, primero debe obtener el bloqueo y debe liberarlo cuando sale o lanza una excepción; cada objeto en Java se puede usar como un bloqueo, que se puede expresar en los siguientes tres formas:
    1. Para los métodos de sincronización ordinarios, el bloqueo es el objeto de instancia actual
    2. Para los métodos de sincronización estática, el bloqueo es el objeto Class de la clase actual
    3. Para el bloque de método sincronizado, el bloqueo es el objeto configurado en corchetes sincronizados

Como puede verse en la especificación de JVM, JVM implementa la sincronización de métodos y la sincronización de bloques de código basándose en la entrada y salida del objeto Monitor. Pero los detalles de implementación de los dos son diferentes.

  1. Sincronización del bloque de código
    Después de la compilación, al insertar monitorenterinstrucciones al principio del bloque de código sincronizado, las monitorexitinstrucciones se insertan al final del método y la excepción;
    cualquier objeto tiene un monitor asociado y cuando el hilo ejecuta la monitorenterinstrucción, intentará obtener el objeto La propiedad del monitor correspondiente es un intento de obtener el bloqueo del objeto,
    cuando y se sostiene un monitor, estará en un estado bloqueado.

  2. Sincronización de método Cuando el
    método sincronizado method_info结构tiene una AAC_synchronizedmarca, el hilo reconocerá la marca y adquirirá el bloqueo correspondiente para lograr la sincronización del método.

Los detalles de implementación de los dos son diferentes, pero son esencialmente la adquisición de un monitor (Monitor) de un objeto.
Cuando un subproceso ejecuta un bloque de código sincronizado o un método sincronizado, el subproceso que ejecuta el método debe obtener primero el monitor del objeto antes de que pueda ejecutarse; el subproceso que no ha obtenido el monitor se bloqueará, entrará en la cola de sincronización y cambiará el estado Blocked. Cuando el subproceso que adquiere con éxito el monitor libera el bloqueo, el subproceso bloqueado en la cola de sincronización se activará para que vuelva a intentar adquirir el monitor.

Los tipos y actualizaciones de cerraduras se presentarán en el próximo artículo.

4. El principio de realización del funcionamiento atómico

Definición de término de CPU

Primero, el procesador garantiza automáticamente la atomicidad de las operaciones básicas de la memoria. Para operaciones complejas (ancho de bus cruzado, múltiples líneas de caché cruzadas y acceso a tablas de páginas cruzadas), el procesador proporciona dos mecanismos: bloqueo de bus y bloqueo de caché para garantizar la atomicidad de sus operaciones.

  1. Utilice bloqueo de bus para garantizar la atomicidad
    Utilice una Lock#señal proporcionada por el procesador Cuando un procesador emite esta señal en el bus, las solicitudes de otros procesadores se bloquearán y el procesador puede monopolizar la memoria compartida.
    El bloqueo del bus bloquea la comunicación entre la CPUT y la memoria Durante el bloqueo, otros procesadores no pueden operar los datos de otras direcciones de memoria, por lo que la sobrecarga del bloqueo del bus es relativamente grande.
    Entonces, el procesador usa el bloqueo de caché para reemplazar el bloqueo de bus en algunas situaciones.

  2. Utilice el bloqueo de caché para garantizar la atomicidad. El
    bloqueo de caché significa que si el área de memoria se almacena en caché en la línea de caché del procesador y se bloquea durante la operación de bloqueo, cuando realiza la operación de bloqueo y escribe en la memoria, el procesador no reclama BLOQUEO en el bus # Señal, pero para modificar la dirección de memoria interna y permitir que su mecanismo de coherencia de caché asegure la atomicidad de la operación, porque el mecanismo de coherencia de caché evita que los datos en el área de memoria almacenados en caché por más de dos procesadores sean modificados al mismo tiempo tiempo, cuando otro procesamiento Cuando el dispositivo vuelve a escribir los datos de una línea de caché bloqueada, invalidará la línea de caché.

  3. Dos situaciones en las que no se puede utilizar el bloqueo de caché

    1. El procesador no admite
    2. Cuando los datos de la operación actual no se pueden almacenar en caché dentro del procesador, o los datos de la operación abarcan varias líneas de caché, el procesador llamará al bloqueo de bus.

5. Cómo implementa Java las operaciones atómicas

Mediante bloqueo y modo CAS cíclico para garantizar el funcionamiento de la atomicidad.

1. Utilice CAS para lograr operaciones atómicas Las operaciones CAS en
Java se implementan utilizando la instrucción CMPXCHG del procesador. La idea básica de spin CAS es realizar un bucle de operaciones CAS hasta que tenga éxito.

  • Desventajas de la implementación CAS de operaciones atómicas
    1. Problema de ABA
      Si un valor resulta ser A, se convierte en B y luego se convierte en A, entonces CAS encontrará que su valor no ha cambiado cuando verifica, pero sí ha cambiado.
      La solución al problema de ABA es usar el número de versión, agregar el número de versión antes de la variable y agregar 1 al número de versión cada vez que se actualiza la variable.
      JDK proporciona AtomicStampedReference para resolver estos problemas.
    2. Tiempo de ciclo prolongado y alto costo
      Si spin CAS no tiene éxito durante mucho tiempo, traerá un gran costo de ejecución para la CPU.
    3. Solo se puede garantizar el funcionamiento atómico de una variable compartida.
      Para múltiples variables compartidas, CAS cíclico no puede garantizar su atomicidad y los bloqueos se pueden usar en este momento.
      La forma complicada es combinar múltiples variables compartidas en una variable compartida para operar, como i_j.
      JDK proporciona la clase AtomicReference para garantizar la atomicidad entre los objetos referenciados, y se pueden colocar varios objetos en un objeto para las operaciones CAS.
  1. Utilice el mecanismo de bloqueo para lograr operaciones atómicas El
    mecanismo de bloqueo asegura que solo el hilo que obtiene el bloqueo pueda operar el área de memoria bloqueada.
    Además del bloqueo sesgado, la JVM implementa bloqueos usando CAS cíclico.Cuando un subproceso realiza un bloque sincronizado, se usa CAS para adquirir el bloqueo, y cuando el subproceso sale del bloque sincronizado, usa CAS para liberar el bloqueo.

Conclusión

Este artículo presenta los principios de implementación de operaciones volátiles, finales, sincronizadas y atómicas. Comprenderlos será más útil para comprender posteriormente los marcos y contenedores de concurrencia.

Referencia:
ejemplo de problema ABA

Supongo que te gusta

Origin blog.csdn.net/u014099894/article/details/102792177
Recomendado
Clasificación