Primero, introduzcamos algunas cerraduras optimistas y cerraduras pesimistas:
Bloqueo pesimista: siempre asuma el peor de los casos. Cada vez que voy a obtener los datos, creo que otros los modificarán, por lo que los bloquearé cada vez que obtenga los datos, para que otros los bloqueen hasta que obtenga el bloqueo. Las bases de datos relacionales tradicionales utilizan muchos mecanismos de bloqueo de estos, como bloqueos de hilos, cerraduras de mesa, etc., bloqueos de lectura, bloqueos de escritura, etc., que están bloqueados antes de realizar operaciones. Por ejemplo, la implementación de la palabra clave sincronizada sincronizada en Java también es un bloqueo pesimista.
Bloqueo optimista: como su nombre indica, significa muy optimista. Cada vez que voy a obtener los datos, creo que otros no los modificarán, por lo que no lo bloquearé. Sin embargo, al actualizar, juzgaré si otros han actualizado los datos durante este período y pueden usar el número de versión y otros mecanismos. Los bloqueos optimistas son adecuados para tipos de aplicaciones de lectura múltiple, lo que puede mejorar el rendimiento. Por ejemplo, la base de datos proporciona un bloqueo optimista similar al mecanismo Write_Condition, pero en realidad todos son proporcionados por bloqueos optimistas. En Java, CAS implementa la clase de variable atómica en Java.util.concurrent.Atomic Package es implementado por CAS utilizando un bloqueo optimista.
Una implementación de Lock-CAS optimista (comparar y intercambiar):
Problemas de bloqueo:
Antes de JDK1.5, Java se basaba en palabras clave sincronizadas para garantizar la sincronización. De esta manera, al usar un protocolo de bloqueo consistente para coordinar el acceso al estado compartido, puede garantizar que, sin importar qué hilo contenga el bloqueo de las variables compartidas, utiliza un método exclusivo para acceder a estas variables. Este es un tipo de bloqueo exclusivo. El bloqueo exclusivo es en realidad una especie de bloqueo pesimista, por lo que se puede decir que sincronizado es un bloqueo pesimista.
El mecanismo de bloqueo pesimista tiene los siguientes problemas:
1. Bajo la competencia múltiple, agregar y lanzar cerraduras conducirá a más retrasos en el contexto y programación, causando problemas de rendimiento.
2. Un hilo que sostiene un bloqueo causará que todos los demás rosces que requieren que este bloqueo cuelgue.
3. Si un hilo con alta prioridad espera un hilo con baja prioridad para liberar el bloqueo, causará una inversión prioritaria, causando riesgos de rendimiento.
En comparación con estos problemas de bloqueos pesimistas, otro bloqueo más efectivo son las cerraduras optimistas. De hecho, el bloqueo optimista es: cada vez que no agrega un bloqueo, pero completa una operación suponiendo que no haya conflicto concurrente. Si el conflicto concurrente falla, intente nuevamente hasta que tenga éxito.
Bloqueo optimista:
El bloqueo optimista se ha mencionado anteriormente, pero en realidad es un tipo de pensamiento. En comparación con los bloqueos pesimistas, las cerraduras optimistas suponen que los datos generalmente no causarán conflictos concurrentes, por lo que cuando los datos se envíen y actualicen, detectará formalmente si los datos tienen conflictos concurrentes. Si se encuentra un conflicto concurrente, se devolverá la información incorrecta del usuario y el usuario decide cómo hacerlo.
El concepto de bloqueo optimista mencionado anteriormente ha explicado sus detalles de implementación específicos: incluye principalmente dos pasos: detección de conflictos y actualización de datos. Uno de los métodos de implementación típicos es comparar e intercambiar (CAS).
CAS:
CAS es una tecnología de bloqueo optimista. Cuando múltiples hilos intentan usar CAS para actualizar la misma variable al mismo tiempo, solo uno de los subprocesos puede actualizar el valor de la variable, mientras que los otros hilos fallan. El hilo fallido no será suspendido, pero se le dirá que esta competencia ha fallado y puede intentarlo nuevamente.
La operación CAS contiene tres operandos: la ubicación de memoria (v) que debe leerse y escribir, el valor original esperado (a) para comparar y el nuevo valor (b) que se escribirá. Si el valor de la posición de memoria V coincide con el valor original esperado A, el procesador actualizará automáticamente el valor de posición al nuevo valor B. De lo contrario, el procesador no hará nada. En cualquier caso, devuelve el valor de esa ubicación antes de la directiva CAS. (En algunos casos especiales de CAS, solo si CAS tiene éxito o no, sin extraer el valor actual). CAS afirma efectivamente que "creo que la posición V debe contener el valor A; si contiene, coloque B en esta posición; de lo contrario, no cambie la posición, solo dígame el valor actual de esta posición". Esto es en realidad lo mismo que el principio de comprobación de conflictos + actualización de datos de bloqueos optimistas.
Permítanme enfatizar aquí que el bloqueo optimista es una especie de pensamiento. CAS es una forma de darse cuenta de esta idea.
Soporte de Java para CAS:
El nuevo java.util.concurrent (JUC) en JDK1.5 se basa en CAS. En comparación con los algoritmos de bloqueo como el sincronizado, CAS es una implementación común de algoritmos sin bloqueo. Por lo tanto, JUC ha mejorado enormemente su rendimiento.
Tome AtomicInteger en java.util.concurrent como ejemplo para ver cómo garantizar la seguridad de los subprocesos sin usar cerraduras. Entendemos principalmente el método getAnstrement, que es equivalente a la operación ++ I.
La clase pública AtomicInteger extiende el número de implementos java.io.erializables {private volátiles intales int; público final int get () {Valor de retorno; } public final int getAndincrement () {for (;;) {int current = get (); int next = corriente + 1; if (compareandSet (actual, siguiente)) return corriente; }} Public Final Boolean CompareandSet (int espere, int actualización) {return unsafe.compareandswapint (this, valueOffset, espere, actualización); }}En el mecanismo sin bloqueos, el valor de campo debe usarse para garantizar que los datos entre los subprocesos sean visibilidad. De esta manera, puede leer directamente cuando obtiene el valor de una variable. Entonces veamos cómo ++ i está hecho.
GetAndIrcrement utiliza la operación CAS, y cada vez que lee datos de la memoria, luego realice la operación CAS en estos datos y el resultado después de +1. Si tiene éxito, el resultado se devolverá; de lo contrario, intente nuevamente hasta que tenga éxito.
CompareandSet utiliza JNI (interfaz nativa de Java) para completar el funcionamiento de las instrucciones de la CPU:
Public Final Boolean CompareandSet (int espere, int actualización) {return unsafe.compareanddswapint (this, valueOffset, esperanza, actualización); }Donde unsafe.com PAREANDSWAPINT (this, valueOffset, espere, actualizar); es similar a la siguiente lógica:
if (this == suppear) {this = update return true; } else {return false; }Entonces, ¿cómo comparar esto == esperar, reemplazar esto = actualizar, comparar a loswapint para lograr la atomicidad de estos dos pasos? Consulte los principios de CAS
Principio de CAS:
CAS se implementa llamando al código JNI. La comparación se implementa mediante el uso de C para llamar a las instrucciones subyacentes de la CPU.
A continuación se explica el principio de implementación de CAS del análisis de la CPU más comúnmente utilizada (Intel X86).
Aquí está el código fuente del método compareSwapInt () del sun.misc.unsafe clase:
Público final Native Boolean Compareandswapint (Object O, Long Offset, int esperado, int x);
Puede ver que esta es una llamada de método local. El código C ++ que llama este método local en el JDK es:
#define list_if_mp (mp) __asm cmmp mp, 0 / __asm je l0 / __asm _Emit 0xf0 / __asm l0: inline jint atomic :: cmpxchg (jint bloschle_value, volátil jint* dest, jint -compare_value) {// alternativo para el intercambio interllockedic. os :: is_mp (); __asm {Mov edx, Dest Mov EcX, Exchange_Value Mov Eax, Compare_Value Lock_IF_MP (MP) CMMPXCHG DWORD PTR [edx], ECX}}Como se muestra en el código fuente anterior, el programa decidirá si se debe agregar un prefijo de bloqueo a la instrucción CMMPXCHG basada en el tipo de procesador actual. Si el programa se ejecuta en un multiprocesador, agregue el prefijo de bloqueo a la instrucción CMMPXCHG. Por el contrario, si el programa se ejecuta en un solo procesador, se omite el prefijo de bloqueo (el procesador único en sí mantiene la consistencia secuencial dentro del procesador único y no requiere el efecto de barrera de memoria proporcionado por el prefijo de bloqueo).
Desventajas de CAS:
1. Preguntas de ABA:
Por ejemplo, si un subproceso, uno saca una posición de memoria V, entonces otro hilo dos también saca A de la memoria, y dos realizan algunas operaciones y se convierten en B, y luego dos giran los datos en la posición V A. En este momento, el hilo uno realiza la operación CAS y descubre que A todavía está en la memoria, y luego uno funciona con éxito. Aunque la operación CAS de hilo de uno es exitosa, puede haber problemas ocultos. Como se muestra a continuación:
Hay una pila implementada con una lista vinculada unidireccional, con la parte superior de la pila A. En este momento, Thread T1 ya sabe que A.Next es B, y luego espera reemplazar la parte superior de la pila con B con CAS:
Head.COMPAREANDSET (A, B);
Antes de que T1 ejecute la instrucción anterior, el hilo T2 interviene, saca A y B de la pila, y luego Pushd, C y A. En este momento, la estructura de la pila es la siguiente, y el objeto B está en estado libre en este momento:
En este momento, es el turno de Hilo T1 para realizar la operación CAS. La detección descubrió que la parte superior de la pila sigue siendo A, por lo que Cas tiene éxito, y la parte superior de la pila se convierte en B, pero de hecho B.Next es nula, por lo que la situación en este momento se vuelve:
Solo hay un elemento B en la pila, y la lista vinculada compuesta de C y D ya no existe en la pila. C y D se tiran sin razón.
A partir de Java 1.5, el paquete atómico del JDK proporciona una clase AtomicStampedRampedReference para resolver el problema ABA. El método comparable de esta clase es verificar primero si la referencia actual es igual a la referencia esperada y si el indicador actual es igual al indicador esperado. Si todos son iguales, la referencia y el valor del indicador se establecen en el valor actualizado dado de manera atómica.
Public Boolean CompareandSet (v esperado Reference, // Referencia esperada v Newreference, // Referencia actualizada int esperado, // bandera esperada int NewStamp // Bandero actualizado)
Código de aplicación real:
AtomicStampedreference privado estático <integer> AtomicStampedRef = new AtomicStampedReference <Integer> (100, 0); ......... AtomicStampedref.com Parearandset (100, 101, Stamp, Stamp + 1);
2. Tiempo de ciclo largo y alto nivel:
Spin Cas (si falla, se ejecutará en bicicleta hasta que tenga éxito) Si falla durante mucho tiempo, traerá una gran sobrecarga de ejecución a la CPU. Si el JVM puede admitir las instrucciones de pausa proporcionadas por el procesador, la eficiencia mejorará en cierta medida. Las instrucciones de pausa tienen dos funciones. Primero, puede retrasar las instrucciones de ejecución de la tubería (des-Pipeline) para que la CPU no consuma demasiados recursos de ejecución. El tiempo de retraso depende de la versión de implementación específica. En algunos procesadores, el tiempo de retraso es cero. En segundo lugar, puede evitar la descarga de la tubería de la CPU causada por la violación del orden de memoria al salir del bucle, mejorando así la eficiencia de ejecución de la CPU.
3. Solo se pueden garantizar operaciones atómicas de una variable compartida:
Al realizar operaciones en una variable compartida, podemos usar el método CAS cíclico para garantizar operaciones atómicas. Sin embargo, al operar múltiples variables compartidas, el CAS cíclico no puede garantizar la atomicidad de la operación. En este momento, puede usar cerraduras, o hay un truco, que es fusionar múltiples variables compartidas en una variable compartida para operar. Por ejemplo, hay dos variables compartidas i = 2, j = a, fusionar ij = 2a y luego usar CAS para operar IJ. A partir de Java 1.5, JDK proporciona la clase Atomicreference para garantizar la atomicidad entre los objetos referenciados. Puede poner varias variables en un objeto para la operación CAS.
CAS y escenarios de uso sincronizado:
1. Para situaciones en las que hay menos competencia de recursos (conflicto de hilo de luz), el uso de un bloqueo de sincronización sincronizado para el bloqueo de subprocesos y las operaciones de conmutación y conmutación entre los estados del núcleo de estado de usuario es una pérdida adicional de recursos de CPU; Si bien CAS se implementa en función del hardware, no necesita ingresar al núcleo, no necesita cambiar los subprocesos, y la posibilidad de que funcione es menor, por lo que se puede obtener un mayor rendimiento.
2. Para situaciones donde la competencia de recursos es grave (conflicto de hilo severo), la probabilidad de girar CAS es relativamente alta, lo que desperdicia más recursos de CPU y es menos eficiente que sincronizado.
Suplemento: Sincronized se ha mejorado y optimizado después de JDK1.6. La implementación subyacente de sincronizado se basa principalmente en la cola de Lock-Free. La idea básica es bloquear después del giro, continuar compitiendo por las cerraduras después del cambio de competencia, sacrificando ligeramente la equidad, pero obteniendo un alto rendimiento. Cuando hay menos conflictos de hilos, se puede obtener un rendimiento similar; Cuando hay serios conflictos de hilos, el rendimiento es mucho más alto que el de CAS.
Implementación del paquete concurrente:
Dado que el CAS de Java tiene ambos semánticos de memoria para lectura volátil y escritura volátil, ahora hay cuatro formas de comunicarse entre los hilos de Java:
1. El rostro A escribe la variable volátil, y luego el hilo B lee la variable volátil.
2. El hilo A escribe la variable volátil, y luego el subproceso B usa CAS para actualizar la variable volátil.
3. El subproceso A usa CAS para actualizar una variable volátil, y luego el hilo B usa CAS para actualizar esta variable volátil.
4. El subproceso A usa CAS para actualizar una variable volátil, y luego el subproceso B lee esta variable volátil.
El CAS de Java utiliza instrucciones atómicas eficientes a nivel de máquina proporcionadas en procesadores modernos, que realizan operaciones de lectura-escritura de cambio en la memoria atómicamente, que es la clave para lograr la sincronización en los multiprocesadores (esencialmente, una máquina de computadora que puede admitir instrucciones atómicas de lectura-escritura de cambio es una máquina equivalente asíncrona que puede realizar una máquina equivalente asíncrona que calcula secuencialmente, calcula las máquinas, los múltiples múltiples modernos. Operaciones de lectura-cambio-escritura en la memoria). Al mismo tiempo, la lectura/escritura y CAS de la variable volátil pueden realizar la comunicación entre los hilos. La integración de estas características juntas forma la piedra angular de la implementación de todo el paquete concurrente. Si analizamos cuidadosamente la implementación del código fuente del paquete concurrente, encontraremos un patrón de implementación general:
1. Primero, declare que la variable compartida es volátil;
2. Luego, use la actualización de condición atómica de CAS para lograr la sincronización entre hilos;
3. Al mismo tiempo, la comunicación entre hilos se logra utilizando la lectura/escritura de volátil y la semántica de memoria de la lectura y escritura volátiles en CAS.
AQS, estructuras de datos sin bloqueo y clases de variables atómicas (clases en el paquete java.util.concurrent.atomic), las clases básicas en estos paquetes concurrentes se implementan utilizando este patrón, y las clases de alto nivel en el paquete concurrente se basan en estas clases básicas para implementar. Desde una perspectiva general, el diagrama de implementación del paquete concurrente es el siguiente:
CAS (asignación de objetos en el montón):
Java llama a new object() para crear un objeto, que se asignará al montón JVM. Entonces, ¿cómo se guarda este objeto en el montón?
En primer lugar, cuando se ejecuta new object() , cuánto espacio necesita este objeto realmente se determina, porque los diversos tipos de datos en Java y cuánto espacio llevan están solucionados (si no tiene claro su principio, por favor, busque en Google usted mismo). Luego, el siguiente trabajo es encontrar un espacio en el montón para almacenar este objeto.
En el caso de un solo hilo, generalmente hay dos estrategias de asignación:
1. Colisión del puntero: esto generalmente es aplicable a la memoria que es absolutamente regular (si la memoria es regular depende de la estrategia de reciclaje de memoria). La tarea de asignar espacio es solo mover el puntero como la distancia del tamaño del objeto en el lado de la memoria libre.
2. Lista gratuita: esto es adecuado para la memoria no regular. En este caso, el JVM mantendrá una lista de memoria para grabar qué áreas de memoria son gratuitas y qué tamaño es. Al asignar espacio a los objetos, vaya a la lista gratuita para consultar el área apropiada y luego asignarlo.
Sin embargo, es imposible que el JVM se ejecute en un solo estado de roscado todo el tiempo, por lo que la eficiencia es demasiado pobre. Dado que no es una operación atómica al asignar la memoria a otro objeto, se requieren al menos los siguientes pasos: encontrar una lista gratuita, asignar memoria, modificar una lista gratuita, etc., que no es segura. También hay dos estrategias para resolver los problemas de seguridad durante la concurrencia:
1. CAS: De hecho, la máquina virtual utiliza CAS para garantizar la atomicidad de la operación de actualización al no volver a intentarlo, y el principio es el mismo que se mencionó anteriormente.
2. TLAB: si se usa CAS, en realidad tendrá un impacto en el rendimiento, por lo que el JVM ha propuesto una estrategia de optimización más avanzada: cada hilo pre-asigna una pequeña memoria en el montón de Java, llamado búfer de asignación de hilos local (TLAB). Cuando el hilo necesita asignar memoria dentro, es suficiente para asignarla directamente a TLAB, evitando los conflictos de subprocesos. Solo cuando la memoria del búfer se use y necesita reasignar la memoria CAS se realizará para asignar un espacio de memoria más grande.
Si la máquina virtual usa TLAB se puede configurar a través -XX:+/-UseTLAB (las versiones JDK5 y posteriores están habilitados de forma predeterminada).
Lo anterior es todo el contenido de este artículo. Espero que sea útil para el aprendizaje de todos y espero que todos apoyen más a Wulin.com.