1. Proponer problemas de sincronización
Supongamos que usamos un procesador de doble núcleo para ejecutar dos hilos A y B, Core 1 ejecuta el subproceso A, y Core 2 ejecuta el subproceso B, ambos hilos ahora tienen que agregar 1 a la variable de miembro I del objeto llamado OBJ. Suponiendo que el valor inicial de I es 0, teóricamente, el valor de I debería convertirse en 2 después de que se ejecuten los dos hilos, pero de hecho es muy probable que el resultado sea 1.
Analicemos las razones ahora. En aras de la simplicidad de análisis, no consideramos la situación de caché. De hecho, hay un caché que aumentará la posibilidad de que el resultado sea 1. El rostro A lee la variable I en la memoria en la unidad de operación aritmética del núcleo 1, luego realiza la operación de adición, y luego escribe el resultado del cálculo a la memoria. Debido a que la operación anterior no es una operación atómica, siempre y cuando el subproceso B lea el valor de I en la memoria antes de que el subproceso A escriba el valor de I agregando 1 de nuevo a la memoria (el valor de I es 0 en este momento), entonces el resultado de I definitivamente parece ser 1. Debido a que el valor de I leída por hilos a y b es 0, y el valor después de agregar 1 a It es 1, los dos hilos escriben 1 a la variable I, que es, el valor de I es 1.
La solución más común es usar la palabra clave de sincronización para bloquear el objeto OBJ con el código que agrega 1 al código I-visible en dos hilos. Hoy presentamos una nueva solución, que es usar clases relacionadas en el paquete atómico para resolverlo.
2. Soporte de hardware atómico
En un solo sistema de procesador (uniprocesador), las operaciones que pueden completarse en una sola instrucción pueden considerarse "operaciones atómicas" porque las interrupciones solo pueden ocurrir entre las instrucciones (porque la programación de los hilos debe completarse a través de interrupciones). Esta es también la razón por la cual algunos sistemas de instrucciones de CPU introducen test_and_set, test_and_clear y otras instrucciones para la exclusión mutua de recursos críticos. Es diferente en la estructura simétrica de múltiples procesadores, ya que múltiples procesadores se ejecutan de forma independiente en el sistema, incluso las operaciones que pueden completarse en una sola instrucción pueden ser alteradas.
En la plataforma X86, la CPU proporciona los medios para bloquear el bus durante la ejecución de instrucciones. Hay un #hlockpin principal en el chip CPU. Si el "bloqueo" del prefijo se agrega a una instrucción en el programa de lenguaje de ensamblaje, el código de la máquina de ensamblaje hará que la CPU disminuya el potencial de #HLockPin al ejecutar esta instrucción, y la liberará hasta el final de esta instrucción, bloqueando así el bus. De esta manera, otras CPU en el mismo bus no pueden acceder a la memoria a través del bus por el momento, asegurando la atomicidad de esta instrucción en un entorno multiprocesador. Por supuesto, no todas las instrucciones se pueden prefijo con bloqueo. Solo agregue, ADC y, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NO, OR, SBB, SUM, XOR, XADD y XCHG Las instrucciones pueden prefijarse con instrucciones de "bloquear" para realizar operaciones atómicas.
La operación central de Atomic es CAS (comparación, implementada utilizando la instrucción CMPXCHG, que es una instrucción atómica). Esta instrucción tiene tres operandos, el valor de memoria V de la variable (la abreviatura del valor), el valor esperado actual e de la variable (la abreviatura de la excepción), el valor U de la variable quiere actualizar (la abreviatura de la actualización). Cuando el valor de memoria es el mismo que el valor esperado actual, el valor actualizado de la variable se sobrescribe por la variable, y el código de pseudo se ejecuta de la siguiente manera.
if (v == e) {v = u return true} else {return false}Ahora usaremos operaciones CAS para resolver los problemas anteriores. El hilo B lee la variable I en la memoria en una variable temporal (suponiendo que el valor leído en este momento es 0), y luego lee el valor de I en la unidad de operación aritmética de Core1. A continuación, agrega 1 para comparar si el valor en la variable temporal es el mismo que el valor actual de i. Si el valor de I en la memoria es el mismo con el valor del resultado en la unidad de operación (es decir, i+1) (tenga en cuenta que esta parte es una operación CAS, es una operación atómica, que no se puede interrumpir y la operación CAS en otros subprocesos no se puede ejecutar al mismo tiempo), de lo contrario la ejecución de instrucciones falla. Si la instrucción falla, significa que el subproceso A ha aumentado el valor de I por 1. De esto podemos ver que si el valor de I lea por ambos subprocesos es 0 al principio, solo la operación de CAS de un hilo puede ser exitosa, porque la operación CAS no se puede ejecutar simultáneamente. Para los subprocesos que no ejecutan las operaciones de CAS, siempre que las operaciones de CAS se ejecuten en bucle, definitivamente tendrá éxito. Puede ver que no hay un bloqueo de hilos, que es esencialmente diferente del principio de sincronización.
3. Introducción al paquete atómico y análisis del código fuente
La característica básica de la clase en el paquete atómico es que en un entorno de múltiples subprocesos, cuando múltiples subprocesos funcionan con una variable única (incluidos tipos básicos y tipos de referencia) al mismo tiempo, es exclusivo, es decir, cuando múltiples subprocesos actualizan el valor de la variable al mismo tiempo, solo un hilo puede tener éxito, y el hilo no exitoso puede continuar intentando como un bloqueo de hilos giratorio y esperar hasta que la ejecución sea exitosa.
Los métodos centrales en la clase Atomic Series llamará a varios métodos locales en la clase insegura. Necesitamos saber primero que una cosa es la clase insegura, con su nombre completo: sun.misc.unsafe. Esta clase contiene una gran cantidad de operaciones en código C, incluidas muchas asignaciones de memoria directa y llamadas de operaciones atómicas. La razón por la que está marcado como no seguro es decirle que una gran cantidad de llamadas de métodos en esta área tendrá riesgos de seguridad y necesitará ser utilizado cuidadosamente, de lo contrario conducirá a graves consecuencias. Por ejemplo, al asignar la memoria a través de inseguras, si especifica ciertas áreas usted mismo, puede hacer que algunos punteros como C ++ cruzen el límite a otros procesos.
Las clases en el paquete atómico se pueden dividir en 4 grupos de acuerdo con el tipo de datos operativos.
AtomicBoolean,AtomicInteger,AtomicLong
Tipos básicos de operaciones atómicas para hilos seguros
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
Operación atómica segura de hilo del tipo de matriz, que no funciona en toda la matriz, sino en un solo elemento en la matriz
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
Operaciones seguras de hilo basadas en tipos básicos (entero largo, entero y tipo de referencia) en los objetos principales de reflexión
AtomicReference,AtomicMarkableReference,AtomicStampedReference
Tipos de referencia seguros de hilo y operaciones atómicas de tipos de referencia que evitan los problemas de ABA
Generalmente usamos AtomicInteger, Atomicreference y AtomicStampedReference. Ahora analicemos el código fuente de entero atómico en el paquete atómico. Los códigos de origen de otras clases son similares en principio.
1. Constructor de parámetros
public atomicInteger (int inicialValue) {value = inicialValue;}Como se puede ver en la función del constructor, el valor se almacena en el valor variable de miembros
Valor privado volátiles int;
El valor variable de miembro se declara como tipo volátil, que muestra la visibilidad en múltiples subprocesos, es decir, la modificación de cualquier hilo se verá inmediatamente en otros hilos.
2. Método de Compara y el valor del valor se pasa a través de este y valor de valores)
Public final Boolean Compareand (int espere, int actualización) {return unsafe.compareanddswapint (this, valueOffset, esperanza, actualización);}Este método es la operación CAS más central
3.getAndSet Method, en el que se llama al método de comparación.
Public final int getandset (int newValue) {for (;;) {int current = get (); if (compareandSet (actual, newValue)) return corriente; }}Si otros subprocesos cambian el valor del valor antes de ejecutar if (compareandSet (actual, newValue), el valor del valor debe ser diferente del valor actual. Si el conjunto de comparación no se ejecuta, solo puede volver a realizar el valor del valor y luego continuar comparando hasta que sea exitoso.
4. Implementación de I ++
public Final int getAndincrement () {for (;;) {int current = get (); int next = corriente + 1; if (compareandSet (actual, siguiente)) return corriente; }}5. Implementación de ++ I
public final int incremementandget () {for (;;) {int current = get (); int next = corriente + 1; if (compareandSet (actual, siguiente)) return Next; }}4. Use el ejemplo de AtomicInteger
El siguiente programa utiliza AtomicInteger para simular el programa de venta de entradas. Los dos programas no venderán el mismo boleto en el resultado de la ejecución, ni venderán los boletos como negativos.
paquete javaleanning; import java.util.concurrent.atomic.atomicInteger; public class sellTickets {AtomicInteger tickets = new AtomicInTEGer (100); class Seller implementa runnable {@Override public void run () {while (tickets.get ()> 0) {int tmp = tickets.get (); si (tarquetset (while (tickets.get ()> 0) {int tmp = tickets.get (); si (tarquetset (while (tickets.get ()> 0) {int tmp = tickets.get (); si (tarquetset (while (tickets.get ()> 0) {int tmp = tickets.get (); si (tarquetset (talletset (ticketset (tickets (tickets () tmp-1)) {System.out.println (Thread.CurrentThread (). "SellerB"). Start ();}}5. Problema de ABA
El ejemplo anterior ejecuta el resultado completamente correcto. Esto se basa en el hecho de que dos (o más) subprocesos operan con datos en la misma dirección. En el ejemplo anterior, ambos hilos operan en boletos en disminución. Por ejemplo, si múltiples hilos realizan operaciones de inscripción de objetos en una cola compartida, entonces los resultados correctos se pueden obtener a través de la clase Atomicreference (este es en realidad el caso de la cola mantenida en AQS). Sin embargo, se pueden inscribir o eliminar múltiples hilos, es decir, la dirección de operación de los datos es inconsistente, por lo que puede ocurrir ABA.
Ahora tomemos un ejemplo relativamente fácil de entender para explicar el problema de ABA. Supongamos que hay dos hilos T1 y T2, y estos dos hilos realizan operaciones de apilamiento y apilamiento en la misma pila.
Usamos la cola definida por atomicreference para guardar la posición superior de la pila
Atomicreference <t> cola;
Suponiendo que el hilo T1 está listo para implementarse, para las operaciones de apilamiento, solo necesitamos actualizar la posición superior de la pila de SP a Newsp a través de la operación de CAS, como se muestra en la Figura 1. Sin embargo, antes de que el hilo T1 ejecute Tail.CompareandSet (SP, Newsp), el sistema realiza la programación de hilos y el hilo T2 comienza la ejecución. T2 realiza tres operaciones: A está fuera de la pila, B está fuera de la pila y luego A está en la pila. At this time, the system starts scheduling again, and the T1 thread continues to perform the stacking operation, but in the view of the T1 thread, the element on the top of the stack is still A (that is, T1 still believes that B is still the next element on the top of the stack A), and the actual situation is shown in Figure 2. T1 will think that the stack has not changed, so tail.compareAndSet(sp,newSP) executes successfully, and the top El puntero de la pila apunta al nodo B. De hecho, B ya no existe en la pila. El resultado después de T1 saca una pila se muestra en la Figura 3, que obviamente no es el resultado correcto.
6. Soluciones a problemas de ABA
Use AtomicMarkableSerference, AtomicStampedReference. Use las dos clases atómicas mencionadas anteriormente para realizar operaciones. Al implementar la instrucción de la comparación, no solo necesitan comparar el valor anterior y el valor esperado del objeto, sino que también necesitan comparar el valor de sello actual (operación) y el valor de sello esperado (operación). Solo cuando lo mismo es cierto, el método de comparación puede tener éxito. Cada vez que la actualización es exitosa, el valor del sello cambiará y la configuración del valor del sello es controlada por el propio programador.
Public Boolean compareSet (v esperadoReference, v NewReference, int esperado, int NewStamp) {par <v> current = par; return esperado == current.reference && esperado == current.stamp && ((newreference == current.reference && newStamp == Current.Stamp) || caspair (current.Of (neweFerference, NewStamp);En este momento, el método de comparación de comparación requiere cuatro parámetros: referencia esperada, NewReference, esperado Samp, NewStamp. Cuando usamos este método, debemos asegurarnos de que el valor de sello esperado no sea el mismo que el valor de sello de actualización. Por lo general, NewStamp = esperado Stamp+1
Tome los ejemplos anteriores
Supongamos que el hilo T1 está antes de la pila: SP apunta a A y el valor del sello es 100.
El hilo T2 se ejecuta: después de que se libera, SP señala B y el valor del sello se convierte en 101.
Después de que B se libera, SP apunta a C, y el valor del sello se convierte en 102.
Después de que A se coloca en la pila, SP señala A y el valor del sello se convierte en 103.
Thread T1 continúa ejecutando la declaración de comparación y descubre que, aunque SP todavía apunta a A, el valor esperado del valor del sello 100 es diferente del valor actual 103. Por lo tanto, el conjunto de comparación falla. Debe obtener el valor de Newsp (en este momento, Newsp apuntará a C), y el valor esperado del valor de sello 103, y luego realizar la operación de comparación de comparación nuevamente. De esta manera, una pila exitosa, SP apuntará a C.
Tenga en cuenta que dado que el conjunto de comparación solo puede cambiar un valor a la vez y no puede cambiar NewReference y NewStamp al mismo tiempo, durante la implementación, una clase de par se define internamente para convertir NewReference y NewStamp en un objeto. Al realizar operaciones CAS, en realidad es una operación en el objeto de par.
par de clase estática privada <T> {referencia t final; Sello de int Final Int; par de pares privados (t reference, int sello) {this.reference = reference; this.stamp = sello; } static <t> par <t> de (t reference, int sello) {return new Par <t> (referencia, sello); }}Para AtomicMarkableReference, el valor del sello es una variable booleana, mientras que el valor de sello en AtomicStampedReference es una variable entera.
Resumir
Lo anterior se trata de la breve discusión de este artículo sobre los principios de implementación y las aplicaciones de los paquetes atómicos en Java. Espero que sea útil para todos. Los amigos interesados pueden continuar referiéndose a otros temas relacionados en este sitio. Si hay alguna deficiencia, deje un mensaje para señalarlo.