Mecanismo de bloqueo y CAS

Mecanismo de bloqueo y CAS

(1) El costo de las cerraduras y las ventajas de las cerraduras libres

El bloqueo es la forma más sencilla de realizar concurrencia y, por supuesto, su costo es el más alto. Los bloqueos de estado del kernel requieren que el sistema operativo realice un cambio de contexto. Bloquear y liberar el bloqueo provocará más cambios de contexto y retrasos en la programación. Los subprocesos que esperan el bloqueo se suspenderán hasta que se libere el bloqueo. Durante el cambio de contexto, las instrucciones y los datos almacenados en caché previamente por la CPU no serán válidos, lo que provocará una gran pérdida de rendimiento. El juicio del sistema operativo sobre las cerraduras de subprocesos múltiples es como dos hermanas discutiendo sobre un juguete. El sistema operativo es el padre que puede determinar quién puede obtener el juguete. Esto es muy lento. Aunque los bloqueos de modo de usuario evitan estos problemas, solo son efectivos cuando no hay competencia real.

Antes de JDK1.5, la sincronización estaba garantizada por la palabra clave sincronizada. Al utilizar un protocolo de bloqueo consistente para coordinar el acceso al estado compartido, puede garantizar que, independientemente del hilo que contenga el bloqueo de la variable guardiana, utilice una forma exclusiva de acceder a estos. Variables. Si varios subprocesos acceden al bloqueo al mismo tiempo, algunos subprocesos se suspenderán. Cuando el subproceso reanuda la ejecución, debe esperar a que otros subprocesos terminen su intervalo de tiempo antes de que se pueda programar para su ejecución. Existe durante la suspensión y reanuda la ejecución. Es mucha sobrecarga. Los bloqueos también tienen otras desventajas: cuando un hilo está esperando el bloqueo, no puede hacer nada. Si un hilo se retrasa mientras se mantiene un bloqueo, todos los hilos que necesitan este bloqueo no se podrán ejecutar. Si la prioridad del hilo bloqueado es alta y la prioridad del hilo que mantiene el bloqueo es baja, la prioridad se invertirá. La siguiente figura muestra el complejo proceso de sincronización:

Inserte la descripción de la imagen aquí

CAS puede resolver este tipo de inconvenientes, entonces, ¿qué es CAS? Para el control de concurrencia, el bloqueo es una estrategia pesimista que bloqueará la ejecución del hilo, mientras que el bloqueo libre es una estrategia optimista. Se asume que no hay conflicto al acceder a los recursos. Como no hay conflicto, no hay necesidad de esperar, y el hilo no. Necesito bloquear. ¿Cómo acceden varios subprocesos a los recursos de la sección crítica juntos? La estrategia sin bloqueo utiliza una tecnología de comparación e intercambio CAS (comparar e intercambiar) para identificar conflictos de subprocesos. Una vez que se detecta un conflicto, la operación actual se repite hasta que no hay conflicto. En comparación con los bloqueos, CAS hará que el diseño del programa sea más complicado, pero debido a su inmunidad inherente al interbloqueo (no hay ningún bloqueo, por supuesto, no habrá subprocesos bloqueados todo el tiempo) y, lo que es más importante, no hay nada que ver con el uso de métodos sin bloqueo. La sobrecarga causada por la competencia no tiene la sobrecarga causada por la programación frecuente entre subprocesos. Tiene un rendimiento superior al enfoque basado en bloqueos, por lo que se ha utilizado ampliamente en la actualidad.

(2) Cerradura optimista y cerradura pesimista

Acabo de mencionar la estrategia pesimista y la estrategia optimista, así que echemos un vistazo a lo que es el bloqueo optimista y el bloqueo pesimista:

Bloqueo optimista (también conocido como sin bloqueo, en realidad no es un bloqueo, sino una idea): piense de manera optimista que otros hilos no modificarán el valor, si se encuentra que el valor se modifica, puede intentarlo nuevamente hasta que tenga éxito . El mecanismo CAS (Compare And Swap) del que vamos a hablar es un bloqueo optimista.

Bloqueo pesimista: cree pesimistamente que otros hilos modificarán el valor. El bloqueo exclusivo es una especie de bloqueo pesimista que, después del bloqueo, puede garantizar que el programa no sea interferido por otros subprocesos durante la ejecución del programa, para obtener el resultado correcto.

(3) mecanismo CAS

El nombre completo del mecanismo CAS es comparar e intercambiar, que se traduce como comparar e intercambiar, y es un conocido algoritmo sin bloqueo. También es una operación a nivel de instrucción de la CPU que es ampliamente compatible con las CPU modernas. Tiene solo una operación atómica, por lo que es muy rápida. Además, CAS evita el problema de solicitar al sistema operativo que adjudique el bloqueo, y se hace directamente dentro de la CPU.

Inserte la descripción de la imagen aquí

CAS tiene tres parámetros operativos:

1. Ubicación de la memoria M (su valor es lo que queremos actualizar)
2. Valor original esperado E (el valor leído de la memoria la última vez)
3. Nuevo valor V (el nuevo valor que se debe escribir)

Proceso de operación CAS: primero lea el valor original de la ubicación de memoria M, márquelo como E, luego calcule el nuevo valor V, compare el valor de la ubicación de memoria actual M con E, si son iguales, significa que no hay nada más en el proceso El hilo ha modificado este valor, por lo que el valor de la ubicación de memoria M se actualiza a V (intercambio) Por supuesto, esto debe hacerse sin problemas de ABA (los problemas de ABA se discutirán más adelante). Si no son iguales, significa que el valor de la ubicación de memoria M ha sido modificado por otros subprocesos, por lo que no se actualiza, y vuelve al inicio de la operación para volver a ejecutarse (giro).

Podemos mirar el algoritmo CAS representado por C:

int cas(long *addr, long old, long new) {
    
    
    if(*addr != old)
        return 0;
    *addr = new;
    return 1;
}

Por lo tanto, cuando varios subprocesos intentan usar CAS para actualizar la misma variable al mismo tiempo, uno de los subprocesos actualizará con éxito el valor de la variable y el resto fallará. El subproceso fallido puede continuar reintentando hasta que tenga éxito. En pocas palabras, el significado de CAS es "lo que creo que debería ser el valor original, si lo es, actualice el valor original al nuevo valor; de lo contrario, no se modificará y dígame cuál es el valor ahora".

Algunas personas pueden tener curiosidad, la operación CAS, primero leer y luego comparar, y luego establecer el valor, tantos pasos, ¿será interferido por otros hilos y causará conflictos entre los pasos? De hecho, no lo hará, porque la operación CAS en el código ensamblador subyacente no se implementa con tres instrucciones, sino solo una instrucción: bloquear cmpxchg (arquitectura x86), por lo que el segmento de tiempo no se robará durante la ejecución de CAS Caso. Pero esto implica otro problema: la operación CAS se basa demasiado en el diseño de la CPU, lo que significa que CAS es esencialmente una instrucción en la CPU. Si la CPU no admite operaciones CAS, CAS no se puede implementar.

Dado que hay un proceso de prueba continuo en CAS, ¿causará un gran desperdicio de recursos? La respuesta es posible, y esta es también una de las deficiencias de CAS. Sin embargo, dado que CAS es un bloqueo optimista, el diseñador debe ser optimista al diseñar. En otras palabras, CAS cree que tiene una probabilidad muy alta de que pueda completar con éxito la operación actual, por lo que en opinión de CAS , Reintentar después de completar (giro) es un evento de pequeña probabilidad.

(4) Problemas con CAS

  1. Problema de ABA
    Debido a que CAS verificará si el valor anterior ha cambiado, aquí hay un problema tan interesante. Por ejemplo, un valor antiguo A se convierte en B y luego en A. Sucede que el valor antiguo no ha cambiado y sigue siendo A, pero en realidad ha cambiado. La solución puede seguir el método de bloqueo optimista que se usa comúnmente en la base de datos y agregar un número de versión para resolverlo. La ruta de cambio original A-> B-> A se convierte en 1A-> 2B-> 3C. AtomicStampedReference se proporciona en el paquete atómico después de Java 1.5 para resolver el problema de ABA, y la solución es así.

  2. Si el tiempo de giro es demasiado largo
    , sincronización sin bloqueo cuando se usa CAS, es decir, el hilo no se suspenderá y girará (no es más que un bucle infinito) para el próximo intento. Si el tiempo de giro aquí es demasiado largo, el rendimiento será excelente. Consumo. Si la JVM puede admitir la instrucción de pausa proporcionada por el procesador, habrá una cierta mejora en la eficiencia.

  3. El funcionamiento atómico
    de una variable compartida solo se puede garantizar, cuando se realiza una operación sobre una variable compartida, CAS puede garantizar su atomicidad, si se operan múltiples variables compartidas, CAS no puede garantizar su atomicidad. Una solución es usar objetos para integrar múltiples variables compartidas, es decir, las variables miembro de una clase son estas variables compartidas. Entonces la operación CAS de este objeto puede asegurar su atomicidad. AtomicReference se proporciona en atomic para garantizar la atomicidad entre los objetos referenciados.

Se puede ver que aunque CAS tiene algunos problemas, se optimiza y soluciona constantemente.

(5) Algunas aplicaciones de CAS

Hay un paquete java.util.concurrent.atomic en jdk. Las clases dentro son todas operaciones sin bloqueo basadas en la implementación CAS. Estas clases atómicas son seguras para subprocesos. El paquete atómico proporciona un total de 13 clases, pertenecientes a 4 tipos de métodos de actualización atómica, a saber, tipo básico de actualización atómica, matriz de actualización atómica, referencia de actualización atómica y atributo de actualización atómica (campo). Las clases en el paquete Atomic son básicamente clases de empaquetado implementadas usando Unsafe. Podemos elegir una clase clásica de la que hablar (escuché que muchas personas entran en contacto con CAS de esta clase), que es la clase AtomicInteger.

Primero podemos ver cómo se inicializa AtomicInteger:

public class AtomicInteger extends Number implements java.io.Serializable {
    
    
    private static final long serialVersionUID = 6214790243416807050L;

    // 很显然AtomicInteger是基于Unsafe类实现的
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    // 属性value值在内存中偏移量
    private static final long valueOffset;

    static {
    
    
        try {
    
    
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
    
     throw new Error(ex); }
    }

    // AtomicInteger本身是个整型,所以属性就是int,被volatile修饰保证线程可见性
    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
    
    
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    
    
    }
}   

Podemos ver varios puntos: La clase insegura (similar a los punteros en lenguaje C) es el núcleo de CAS, que es el núcleo de AtomicInteger. valueOffset es la dirección de desplazamiento del valor en la memoria y Unsafe proporciona el método de operación correspondiente. El valor es modificado por la palabra clave volatile para asegurar la visibilidad del hilo, esta es la variable que realmente almacena el valor.

Echemos un vistazo a cómo se implementa el método getAndIncrement más utilizado en AtomicInteger:

public final int getAndIncrement() {
    
    
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

Obviamente, llamó al método getAndAddInt en inseguro, luego saltamos a este método:

/**
 * Atomically adds the given value to the current value of a field
 * or array element within the given object <code>o</code>
 * at the given <code>offset</code>.
 *
 * @param o object/array to update the field/element in
 * @param offset field/element offset
 * @param delta the value to add
 * @return the previous value
 * @since 1.8
 */
public final int getAndAddInt(Object o, long offset, int delta) {
    
    
    int v;
    do {
    
    
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

 /**
  * Atomically update Java variable to <tt>x</tt> if it is currently
  * holding <tt>expected</tt>.
  * @return <tt>true</tt> if successful
  */
 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

Podemos ver que el ciclo do-while en el método getAndAddInt es equivalente a la parte spin en CAS. Si el reemplazo falla, continuará intentándolo hasta que tenga éxito. El código central del CAS real (el proceso de comparar e intercambiar) llama al código C ++ local en el método nativo de compareAndSwapInt:

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);
	//调用Atomic操作
	//先去获取一次结果,如果结果和现在不同,就直接返回,因为有其他人修改了;否则会一直尝试去修改。直到成功。
	return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

Finalmente vimos el comando con el que estamos familiarizados: cmpxchg. En otras palabras, el contenido del que hablamos antes ha sido completamente concatenado. La esencia de CAS es una instrucción de CPU, y la implementación de todas las clases atómicas llama a esta instrucción capa por capa para lograr la operación sin bloqueo que necesitamos.

Suponga que hay un nuevo AtomicInteger (0); ahora hay subproceso 1 y subproceso 2 para realizar la operación getAndAddInt en él al mismo tiempo.
1) El hilo 1 obtiene primero el valor 0, y luego el hilo cambia;
2) El hilo 2 obtiene el valor 0, en este momento llamando a Unsafe para comparar el valor en la memoria también es 0, es relativamente exitoso, es decir, se realiza la operación de actualización de +1, es decir El valor actual es 1. Cambio de subproceso;
3) El subproceso 1 se reanuda en ejecución, utilizando CAS para encontrar que su valor es 0, pero es 1 en la memoria. Obtener: el valor es modificado por otro hilo en este momento, y no puedo modificarlo;
4) El hilo 1 no juzga, continúa repitiendo el valor y juzga. Debido a que volátil modifica el valor, el valor obtenido también es 1. Esto está realizando la operación CAS, y se encuentra que el valor de expectativa y la memoria en este momento es igual, la modificación es exitosa y el valor es 2;
5) En el proceso del cuarto paso, incluso si hay subproceso 3 para apropiarse de recursos durante la operación CAS, todavía no puede La preferencia es exitosa porque compareAndSwapInt es una operación atómica.

30 de mayo de 2020

Supongo que te gusta

Origin blog.csdn.net/weixin_43907422/article/details/106423364
Recomendado
Clasificación