JUC Conferencia 10: Explicación detallada de CAS, clases atómicas y inseguras

JUC Conferencia 10: Explicación detallada de CAS, clases atómicas y inseguras

La mayoría de las clases en JUC se implementan a través de volátil y CAS. CAS esencialmente proporciona una solución sin bloqueo, mientras que Synchronized y Lock son soluciones de bloqueo mutuamente excluyentes; las clases atómicas de Java esencialmente usan CAS, y la capa inferior de CAS es implementada por la clase Unsafe . Este artículo es la décima conferencia de JUC y explicará en detalle CAS, clases inseguras y atómicas.

1. Comprenda las preguntas de la entrevista de las principales empresas BAT.

Continúe con estas preguntas, que le ayudarán en gran medida a comprender mejor los puntos de conocimiento relevantes.

  • ¿Cuáles son los métodos de implementación de la seguridad de subprocesos?Bloqueo mutex, sin bloqueo, cierre de pila, ThreadLocal
  • ¿Qué es CAS?
  • Ejemplos de uso de CAS, ¿da ejemplos combinados con AtomicInteger?
  • ¿Cuáles son los problemas con CAS?
  • ¿Qué soluciones proporciona Java para resolver estos problemas?
  • ¿La implementación subyacente de AtomicInteger? CAS+volatile
  • Por favor, explique su comprensión de la clase Insegura.
  • Cuénteme sobre su comprensión de las clases atómicas de Java. Contiene 13 categorías y 4 grupos de categorías. Cuéntenos sobre sus funciones y escenarios de uso.
  • ¿Qué es AtomicStampedReference?
  • ¿Cómo resuelve AtomicStampedReference ABA? El par se usa internamente para almacenar valores de elementos y sus números de versión
  • ¿Qué otras clases en Java pueden resolver el problema de ABA? AtomicMarkableReference

2、CAS

Como mencionamos anteriormente, los métodos de implementación seguros para subprocesos incluyen:

  • Sincronización mutuamente excluyente: sincronizada y ReentrantLock
  • Sincronización sin bloqueo: CAS, AtomicXXXX
  • Sin solución de sincronización: cierre de pila, Thread Local, código reentrante

Para obtener más información, consulte: JUC Conferencia 2: Bases teóricas de la concurrencia de Java: modelo de memoria Java (JMM) y subprocesos Aquí nos centraremos en CAS.

2.1 ¿Qué es CAS?

El nombre completo de CAS es Compare-And-Swap, que literalmente significa comparación e intercambio. Es una instrucción atómica de la CPU. Su función es permitir que la CPU primero compare si dos valores son iguales y luego actualice atómicamente el valor de una determinada posición. Después de la investigación, se descubrió que su implementación se basa en las instrucciones de ensamblaje de la plataforma de hardware, lo que significa que CAS se implementa mediante hardware, la JVM solo encapsula llamadas de ensamblaje y esas clases AtomicInteger usan estas interfaces encapsuladas. Explicación simple: la operación CAS requiere ingresar dos valores, un valor antiguo (el valor esperado antes de la operación) y un valor nuevo. Durante la operación, primero compare si el valor anterior ha cambiado. Si no hay cambios, cámbielo al nuevo valor. , no se cambiará si hay un cambio.

Las operaciones de CAS son atómicas, por lo que cuando varios subprocesos utilizan CAS para actualizar datos al mismo tiempo, no se necesitan bloqueos. CAS se utiliza ampliamente en el JDK para actualizar datos y evitar bloqueos (bloqueos pesados ​​sincronizados) para mantener actualizaciones atómicas.

Creo que todo el mundo está familiarizado con SQL, que es similar a la actualización condicional en SQL update set id=3 from table where id=2:. Debido a que una única instrucción SQL se ejecuta de forma atómica, si varios subprocesos ejecutan esta instrucción SQL al mismo tiempo, solo uno se puede actualizar correctamente.

2.2 Ejemplos de uso de CAS

Si no se usa CAS, en condiciones de alta concurrencia, si varios subprocesos modifican el valor de una variable al mismo tiempo, necesitamos un bloqueo sincronizado (algunas personas pueden decir que Lock se puede usar para bloquear, y el AQS subyacente de Lock también adquiere bloqueos). basado en CAS).

public class Test {
    
    
    private int i=0;
    public synchronized int add(){
    
    
        return i++;
    }
}

Java nos proporciona la clase atómica AtomicInteger (la capa inferior actualiza datos según CAS), que puede lograr coherencia de datos en escenarios de concurrencia de subprocesos múltiples sin bloqueo.

public class Test {
    
    
    private  AtomicInteger i = new AtomicInteger(0);
    public int add(){
    
    
        return i.addAndGet(1);
    }
}

CAS no se utilizará directamente en el proyecto, sino varias clases de herramientas implementadas por CAS: por ejemplo: AtomicInteger, AtomicReference, LongAdder, etc.

  • Escenario 1: utilice AtomicInteger para contar el número de SPU, contar el número de filas analizadas de Excel y contar los lotes de envío de mensajes;
  • Escenario 2: utilice LongAdder para contar la cantidad de solicitudes de caché;
  • Escenario 3: la aplicación más clásica de AtomicInteger, contando el número de subprocesos;
  • Escenario 4: use AtomicReference para almacenar temporalmente excepciones y guardar información del objeto. Al ensamblar la SPU, use AtomicReference para guardar información de marca e información del modelo.

2.3 Cuestiones del CAS

El modo CAS es un bloqueo optimista y el modo sincronizado es un bloqueo pesimista. Por lo tanto, utilizar CAS para resolver problemas de concurrencia suele tener un mejor rendimiento.

Sin embargo, existen varios problemas con el uso de CAS:

1. Problema ABA

Porque CAS necesita verificar si el valor ha cambiado al operar el valor. Por ejemplo, si no hay cambios, se actualizará. Sin embargo, si un valor es originalmente A, se convierte en B y luego se convierte en A, entonces cuando se usa CAS para verificar, encontrará que su valor no ha cambiado, pero en realidad sí ha cambiado.

La solución al problema de ABA es utilizar números de versión. Agregue el número de versión delante de la variable y agregue 1 al número de versión cada vez que se actualice la variable, luego A->B->A se convertirá en 1A->2B->3A.

A partir de Java 1.5, el paquete Atomic de JDK proporciona una clase AtomicStampedReference para resolver el problema ABA. Lo que hace el método compareAndSet de esta clase es primero verificar si la referencia actual es igual a la referencia esperada, y verificar si el indicador actual es igual al indicador esperado, y si todos son iguales, establecer atómicamente el valor de la referencia y el bandera al valor actualizado dado.

2. Tiempo de ciclo largo y alto costo

Si spin CAS falla durante mucho tiempo, generará una gran sobrecarga de ejecución para la CPU. Si la JVM puede admitir la instrucción de pausa proporcionada por el procesador, la eficiencia mejorará hasta cierto punto. La instrucción de pausa tiene dos funciones: primero, puede retrasar el comando de ejecución de la tubería (de-pipeline) para que la CPU no consuma demasiados recursos de ejecución. El tiempo de demora depende de la versión de implementación específica. En algunos procesadores, el tiempo de demora es cero; en segundo lugar, puede evitar que la canalización de la CPU se borre (CPU Pipeline Flush) debido a un conflicto en el orden de la memoria (Infracción del orden de la memoria) al salir del bucle, mejorando así la eficiencia de ejecución de la CPU.

3. Solo se pueden garantizar operaciones atómicas en una variable compartida.

Al realizar una operación en una variable compartida, podemos usar CAS cíclico para garantizar operaciones atómicas. Sin embargo, cuando operamos en múltiples variables compartidas, CAS cíclico no puede garantizar la atomicidad de la operación. En este caso, se pueden usar bloqueos.

Otro truco consiste en fusionar varias variables compartidas en una variable compartida para su operación. Por ejemplo, hay dos variables compartidas i = 2, j = a, fusionan ij = 2a y luego usan CAS para operar ij.

A partir de Java 1.5, JDK proporciona la clase AtomicReference para garantizar la atomicidad entre los objetos de referencia, de modo que se puedan colocar múltiples variables en un objeto para realizar operaciones CAS .

3. Explicación detallada de la clase UnSafe

Arriba aprendimos que las clases atómicas de Java se implementan a través de la clase UnSafe. Esta sección analiza principalmente la clase UnSafe. La clase UnSafe se usa ampliamente en operaciones CAS en JUC.

Unsafe es una clase ubicada en el paquete sun.misc. Proporciona principalmente algunos métodos para realizar operaciones inseguras de bajo nivel, como acceso directo a los recursos de memoria del sistema, administración independiente de recursos de memoria, etc. Estos métodos mejoran la eficiencia operativa de Java y mejorar Java Las capacidades de operación de recursos subyacentes del lenguaje juegan un papel importante. Sin embargo, dado que la clase Unsafe le da al lenguaje Java la capacidad de operar el espacio de memoria de manera similar a los punteros del lenguaje C, esto sin duda aumenta el riesgo de problemas relacionados con los punteros en el programa. El uso excesivo e incorrecto de la clase Unsafe en un programa aumentará la probabilidad de que se produzcan errores en el programa, haciendo que un lenguaje seguro como Java deje de ser "seguro", por lo que debe utilizar Unsafe con precaución.

Aunque los métodos de esta clase son públicos, no hay forma de utilizarlos y la documentación de la API del JDK no proporciona ninguna explicación sobre los métodos de esta clase. Con todo, el uso de clases inseguras está restringido. Sólo el código autorizado puede obtener instancias de esta clase. Por supuesto, las clases en la biblioteca JDK se pueden usar a voluntad.

Primero veamos esta imagen para ver la función general de la clase UnSafe:

imagen

Como se muestra en la figura anterior, las API proporcionadas por Unsafe se pueden dividir aproximadamente en operaciones de memoria, CAS, relacionadas con clases, operaciones de objetos, programación de subprocesos, adquisición de información del sistema, barreras de memoria, operaciones de matriz, etc. A continuación se describirán sus funciones relacionadas. Métodos y escenarios de aplicación.Introducción detallada.

3.1 Inseguro y CAS

Código descompilado:

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt){
    
    
    int i;
    do
      	i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
    return i;
}

public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2){
    
    
    long l;
    do
      	l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    return l;
}

public final int getAndSetInt(Object paramObject, long paramLong, int paramInt){
    
    
    int i;
    do
      	i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, paramInt));
    return i;
}

public final long getAndSetLong(Object paramObject, long paramLong1, long paramLong2){
    
    
    long l;
    do
      	l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, paramLong2));
    return l;
}

public final Object getAndSetObject(Object paramObject1, long paramLong, Object paramObject2){
    
    
    Object localObject;
    do
      	localObject = getObjectVolatile(paramObject1, paramLong);
    while (!compareAndSwapObject(paramObject1, paramLong, localObject, paramObject2));
    return localObject;
}

En el código fuente se descubrió que el método spin se usa internamente para la actualización de CAS (mientras el bucle realiza la actualización de CAS, si la actualización falla, el bucle lo volverá a intentar).

También se descubrió en la clase Unsafe que las operaciones atómicas en realidad solo admiten los siguientes tres métodos.

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

Descubrimos que Unsafe solo proporciona 3 métodos CAS: compareAndSwapObject, compareAndSwapInt y compareAndSwapLong. Todos son métodos nativos.

3.2 Capa inferior insegura

Echemos un vistazo al método compareAndSwap * de Unsafe para implementar operaciones CAS. Es un método local y la implementación se encuentra en unsafe.cpp.

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
    UnsafeWrapper("Unsafe_CompareAndSwapInt");
    oop p = JNIHandles::resolve(obj);
    jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
    return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

Puede ver que Atomic::cmpxchgimplementa operaciones de comparación y reemplazo. El parámetro x es el valor a actualizar y el parámetro e es el valor de la memoria original.

Si es Linux x86, Atomic::cmpxchgel método se implementa de la siguiente manera:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    
    
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

La implementación de x86 en Windows es la siguiente:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    
    
    int mp = os::isMP(); //判断是否是多处理器
    _asm {
    
    
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

Si es un multiprocesador, agregue el prefijo de bloqueo a la instrucción cmpxchg. De lo contrario, omita el prefijo de bloqueo (un solo procesador no necesitará el efecto de barrera de memoria proporcionado por el prefijo de bloqueo). El prefijo de bloqueo aquí utiliza el bloqueo del bus del procesador ( los procesadores más recientes utilizan bloqueos de caché en lugar de bloqueos de bus para mejorar el rendimiento ).

cmpxchg(void* ptr, int old, int new), si los valores de ptr y old son iguales, escribe nuevo en la memoria de ptr, de lo contrario devuelve el valor de ptr. Toda la operación es atómica. En la plataforma Intel, se usa lock cmpxchg para implementarlo y lock se usa para activar el bloqueo de caché, de modo que si otro hilo quiere acceder a la memoria de ptr, será bloqueado.

3.3 Otras funciones de Unsafe

Unsafe proporciona operaciones a nivel de hardware, como obtener la ubicación de una propiedad en la memoria, como modificar el valor del campo de un objeto, incluso si es privado. Sin embargo, el propio Java está diseñado para proteger las diferencias subyacentes y rara vez existe tal necesidad de desarrollo general.

Da dos ejemplos, por ejemplo:

public native long staticFieldOffset(Field paramField);

Este método se puede utilizar para obtener el desplazamiento de la dirección de memoria de un campo de parámetro determinado. Este valor es único y fijo para el campo determinado.

Otro ejemplo:

public native int arrayBaseOffset(Class paramClass);
public native int arrayIndexScale(Class paramClass);

El primer método se utiliza para obtener la dirección de desplazamiento del primer elemento de la matriz y el último método se utiliza para obtener el factor de conversión de la matriz, que es la dirección incremental de los elementos de la matriz.

Finalmente, veamos tres métodos:

public native long allocateMemory(long paramLong);
public native long reallocateMemory(long paramLong1, long paramLong2);
public native void freeMemory(long paramLong);

Se utilizan para asignar memoria, expandir memoria y liberar memoria respectivamente.

Para más funciones relacionadas, te recomiendo leer este artículo: Del equipo técnico de Meituan: Clase mágica de Java: Análisis de aplicaciones inseguras

4、Entero atómico

4.1 Ejemplos de uso

Tomando AtomicInteger como ejemplo, API de uso común:

public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,并设置新的值
public final int getAndIncrement() //获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
void lazySet(int newValue) //最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

En comparación con las ventajas de Integer, las variables se pueden incrementar en subprocesos múltiples:

private volatile int count = 0;

// 若要线程安全执行执行 count++,需要加锁
public synchronized void increment() {
    
    
    count++;
}
public int getCount() {
    
    
    return count;
}

Después de usar AtomicInteger:

private AtomicInteger count = new AtomicInteger();
public void increment() {
    
    
    count.incrementAndGet();
}

// 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
public int getCount() {
    
    
    return count.get();
}

4.2 Análisis del código fuente

public class AtomicInteger extends Number implements java.io.Serializable {
    
    
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
    
    
        try {
    
    
            //用于获取value字段相对当前对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
    
     throw new Error(ex); }
    }

    private volatile int value;

    //返回当前值
    public final int get() {
    
    
        return value;
    }

    //递增加detla
    public final int getAndAdd(int delta) {
    
    
        //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //递增加1
    public final int incrementAndGet() {
    
    
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
		...
}

Podemos ver que la capa inferior de AtomicInteger usa variables volátiles y CAS para cambiar datos.

  • Volátil garantiza la visibilidad de los subprocesos. Cuando varios subprocesos son concurrentes, si un subproceso modifica datos, puede garantizar que otros subprocesos puedan ver el valor modificado inmediatamente;
  • CAS garantiza la atomicidad de las actualizaciones de datos.

5. Extendido a todas las clases atómicas: 12 en total

En JDK se proporcionan 12 clases de operaciones atómicas.

5.1 Tipos básicos de actualizaciones atómicas

Utilice métodos atómicos para actualizar tipos básicos. El paquete Atomic proporciona las siguientes tres clases.

  • AtomicBoolean: tipo booleano de actualización atómica.
  • AtomicInteger: tipo entero de actualización atómica.
  • AtomicLong: entero largo de actualización atómica.

Los métodos proporcionados por las tres clases anteriores son casi idénticos. Puede consultar los métodos relacionados en AtomicInteger arriba.

5.2 Matriz de actualización atómica

Para actualizar un elemento en una matriz de forma atómica, el paquete Atomic proporciona las siguientes tres clases:

  • AtomicIntegerArray: actualiza atómicamente elementos en una matriz de enteros.
  • AtomicLongArray: actualiza atómicamente elementos en una matriz de enteros largos.
  • AtomicReferenceArray: actualiza atómicamente elementos en una matriz de tipo de referencia.

Los métodos más utilizados de estas tres clases son los dos métodos siguientes:

  • get (int index): obtiene el valor del elemento con índice index.
  • compareAndSet(int i, E expect, E update): establece atómicamente el elemento en la posición i de la matriz en el valor de actualización si el valor actual es igual al valor esperado.

Dé un ejemplo de AtomicIntegerArray:

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Demo5 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        AtomicIntegerArray array = new AtomicIntegerArray(new int[] {
    
     0, 0 });
        System.out.println(array);
        System.out.println(array.getAndAdd(1, 2));
        System.out.println(array);
    }
}

Resultado de salida:

[0, 0]
0
[0, 2]

5.3 Tipo de referencia de actualización atómica

El paquete Atomic proporciona las siguientes tres clases:

  • AtomicReference: tipo de referencia de actualización atómica.
  • AtomicStampedReference: tipo de referencia de actualización atómica, utiliza internamente Pair para almacenar valores de elementos y sus números de versión.
  • AtomicMarkableReferce: actualiza atómicamente un tipo de referencia con un bit de marca.

Los métodos proporcionados por estas tres clases son similares: primero, construya un objeto de referencia, luego establezca el objeto de referencia en la clase Atomic y luego llame a algunos métodos como compareAndSet para realizar operaciones atómicas. Todos los principios se basan en una implementación insegura, pero AtomicReferenceFieldUpdater es ligeramente diferente Update Los campos deben modificarse con volátil.

Dé un ejemplo de AtomicReference:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
    
    
    
    public static void main(String[] args){
    
    

        // 创建两个Person对象,它们的id分别是101和102。
        Person p1 = new Person(101);
        Person p2 = new Person(102);
        // 新建AtomicReference对象,初始化它的值为p1对象
        AtomicReference ar = new AtomicReference(p1);
        // 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
        ar.compareAndSet(p1, p2);

        Person p3 = (Person)ar.get();
        System.out.println("p3 is "+p3);
        System.out.println("p3.equals(p1)="+p3.equals(p1));
    }
}

class Person {
    
    
    volatile long id;
    public Person(long id) {
    
    
        this.id = id;
    }
    public String toString() {
    
    
        return "id:"+id;
    }
}

Salida de resultados:

p3 is id:102
p3.equals(p1)=false

Descripción de resultados:

  • Al crear un nuevo objeto AtomicReference ar, inicialícelo en p1.
  • A continuación, configúrelo a través de la función CAS. Si el valor de ar es p1, configúrelo en p2.
  • Finalmente, obtenga el objeto correspondiente a ar e imprima el resultado. El resultado de p3.equals(p1) es falso. Esto se debe a que Person no anula el método equals(), sino que utiliza el método equals() heredado de Object.java; y equals() en Object.java en realidad llama a "= =" se usa para comparar dos objetos, es decir, para comparar si las direcciones de los dos objetos son iguales.

5.4 Clase de campo de actualización atómica

El paquete Atomic proporciona cuatro clases para actualizaciones de campos atómicos:

  • AtomicIntegerFieldUpdater: Actualizador para actualizar atómicamente campos enteros.
  • AtomicLongFieldUpdater: actualizador para actualizar atómicamente campos de enteros largos.
  • AtomicReferenceFieldUpdater: se mencionó anteriormente y no se repetirá aquí.

Estas cuatro clases se utilizan de manera similar, actualizando atómicamente los valores de campo en función de la reflexión. Para actualizar una clase de campo atómicamente se requieren dos pasos:

  • El primer paso es que, debido a que las clases de campo de actualización atómica son todas clases abstractas, debes usar el método estático newUpdater() para crear un actualizador cada vez que lo uses, y debes configurar la clase y los atributos que deseas actualizar.
  • En el segundo paso, los campos de la clase actualizada deben modificarse con public volatile.

Por ejemplo:

public class TestAtomicIntegerFieldUpdater {
    
    

    public static void main(String[] args){
    
    
        TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
        tIA.doIt();
    }

    public AtomicIntegerFieldUpdater<DataDemo> updater(String name){
    
    
        return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class, name);

    }

    public void doIt(){
    
    
        DataDemo data = new DataDemo();
        System.out.println("publicVar = "+ updater("publicVar").getAndAdd(data, 2));
        /*
         * 由于在DataDemo类中属性value2/value3,在TestAtomicIntegerFieldUpdater中不能访问
         * */
        //System.out.println("protectedVar = "+updater("protectedVar").getAndAdd(data,2));
        //System.out.println("privateVar = "+updater("privateVar").getAndAdd(data,2));

        //System.out.println("staticVar = "+updater("staticVar").getAndIncrement(data));//报java.lang.IllegalArgumentException
        /*
         * 下面报异常:must be integer
         * */
        //System.out.println("integerVar = "+updater("integerVar").getAndIncrement(data));
        //System.out.println("longVar = "+updater("longVar").getAndIncrement(data));
    }

}

class DataDemo{
    
    
    public volatile int publicVar=3;
    protected volatile int protectedVar=4;
    private volatile  int privateVar=5;

    public volatile static int staticVar = 10;
    //public  final int finalVar = 11;

    public volatile Integer integerVar = 19;
    public volatile Long longVar = 18L;

}

Hablemos de algunas pequeñas restricciones y restricciones en el uso de AtomicIntegerFieldUpdater, las siguientes restricciones:

  • El campo debe ser de tipo volátil y se garantiza que será visible inmediatamente cuando se comparten variables entre subprocesos. Por ejemplo: volatile int value = 3;
  • El tipo de descripción del campo (modificador público/protegido/predeterminado/privado) es coherente con la relación entre la persona que llama y el campo del objeto de operación. Es decir, la persona que llama puede operar directamente los campos del objeto, por lo que puede realizar operaciones atómicas mediante la reflexión. Pero para los campos de la clase principal, la subclase no puede operar directamente, aunque la subclase puede acceder a los campos de la clase principal;
  • Solo puede ser una variable de instancia, no una variable de clase, lo que significa que no se puede agregar la palabra clave estática;
  • Solo puede ser una variable modificable, no una variable final, porque la semántica de final es que no se puede modificar. De hecho, la semántica de final entra en conflicto con volátil, y estas dos palabras clave no pueden existir al mismo tiempo;
  • Para AtomicIntegerFieldUpdater y AtomicLongFieldUpdater, solo se pueden modificar los campos de tipo int/long y sus tipos de empaquetado (Integer/Long) no se pueden modificar. Si desea modificar el tipo de paquete, debe utilizar AtomicReferenceFieldUpdater.

6. Hablemos de AtomicStampedReference resolviendo el problema ABA de CAS.

6.1 AtomicStampedReference resuelve el problema ABA

AtomicStampedReference mantiene principalmente un objeto de par que contiene una referencia de objeto y un "sello" entero que se puede actualizar automáticamente para resolver problemas de ABA.

public class AtomicStampedReference<V> {
    
    
    private static class Pair<T> {
    
    
        final T reference;  //维护对象引用
        final int stamp;  //用于标志版本
        private Pair(T reference, int stamp) {
    
    
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
    
    
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair;
    ....
    
    /**
      * expectedReference :更新之前的原始值
      * newReference : 将要更新的新值
      * expectedStamp : 期待更新的标志版本
      * newStamp : 将要更新的标志版本
      */
    public boolean compareAndSet(V expectedReference,
                             V newReference,
                             int expectedStamp,
                             int newStamp) {
    
    
        // 获取当前的(元素值,版本号)对
        Pair<V> current = pair;
        return
            // 引用没变
            expectedReference == current.reference &&
            // 版本号没变
            expectedStamp == current.stamp &&
            // 新引用等于旧引用
            ((newReference == current.reference &&
            // 新版本号等于旧版本号
            newStamp == current.stamp) ||
            // 构造新的Pair对象并CAS更新
            casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
    
    
        // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
  • Si el valor del elemento y el número de versión no han cambiado y son los mismos que los nuevos, devuelve verdadero;
  • Si el valor del elemento y el número de versión no han cambiado y no son exactamente iguales a los nuevos, construya un nuevo objeto Pair y ejecute CAS para actualizar el par.

Se puede ver que la implementación en Java es consistente con la solución ABA de la que hablamos anteriormente.

  • Primero, utilice el control del número de versión;
  • En segundo lugar, la referencia del nodo (Par) no se reutiliza y cada vez se crea un nuevo Par como objeto de comparación CAS en lugar de reutilizar el anterior;
  • Finalmente, el valor del elemento y el número de versión se pasan externamente en lugar de la referencia del nodo (Par).

6.2 Ejemplos de uso

public class AtomicTester {
    
    

    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);

    public static void main(String[] args){
    
    
        first().start();
        second().start();
    }

    private static Thread first() {
    
    
        return new Thread(() -> {
    
    
            System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference());
            int stamp = atomicStampedRef.getStamp(); //获取当前标识别
            try {
    
    
                Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
        },"主操作线程");
    }

    private static Thread second() {
    
    
        return new Thread(() -> {
    
    
            Thread.yield(); // 确保thread-first 优先执行
            atomicStampedRef.compareAndSet(1, 2, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
            atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
            System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
        },"干扰线程");
    }
}

Resultado de salida:

操作线程Thread[主操作线程,5,main],初始值 a = 1
操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2
操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1
操作线程Thread[主操作线程,5,main],CAS操作结果: false

6.3 ¿Qué otras clases en Java pueden resolver el problema de ABA?

AtomicMarkableReference, no mantiene un número de versión, pero mantiene una marca de tipo booleano. El valor de la marca ha sido modificado. Obtenga más información.

7. Artículos de referencia

Supongo que te gusta

Origin blog.csdn.net/qq_28959087/article/details/133274067
Recomendado
Clasificación