La tercera parte de JAVA multihilo (tres) variables atómicas y mecanismo de sincronización sin bloqueo

Portal de notas concurrentes:
1.0 Programación concurrente-Mapa mental
2.0 Programación concurrente-Fundamentos de seguridad de subprocesos
3.0 Programación concurrente-Módulo de construcción básico
4.0 Programación concurrente-Ejecución de tareas-Futuro
5.0 Programación concurrente-Rendimiento y escalabilidad de subprocesos múltiples
6.0 Programación concurrente- Bloqueo explícito y
programación concurrente sincronizada 7.0-AbstractQueuedSynchronizer
8.0 programación concurrente-variables atómicas y mecanismo de sincronización sin bloqueo

Muchas clases de paquetes simultáneos de JAVA, como Semaphorey ConcurrentLinkedQueue, proporcionan synchronizedmayor rendimiento y escalabilidad que los mecanismos. La fuente principal de esta mejora del rendimiento es 原子变量y 非阻塞同步机制la aplicación.

Desventajas de las cerraduras

Programación de gastos generales

Cuando varios subprocesos compiten por bloqueos, la JVM necesita usar la función del sistema operativo para suspender algunos subprocesos y reanudar la ejecución más tarde. Cuando un subproceso reanuda la ejecución, debe esperar a que otros subprocesos ejecuten su intervalo de tiempo antes de poder programar su ejecución. Hay mucha sobrecarga en el proceso de suspensión y reanudación de subprocesos, y hay una interrupción a largo plazo.

volatileLimitaciones de

volatileLas variables son un mecanismo de sincronización más ligero, porque operaciones como el cambio de contexto o la programación de subprocesos no ocurrirán cuando se utilicen estas variables. Sin embargo, volatileaunque se proporcionan garantías de visibilidad, no se pueden utilizar para construir operaciones de cumplimiento atómico.

Por ejemplo: i++problema que aumenta a sí mismo. Parece una operación atómica, pero en realidad contiene tres operaciones independientes:

  • Obtener el valor actual de la variable
  • Incrementar el valor en 1
  • Escribe un nuevo valor

Hasta ahora, la única forma de lograr esta operación atómica es bloquear. Lo mismo puede causar 调度开销problemas.

Problema de bloqueo

Cuando un hilo está esperando el bloqueo, no puede hacer nada más. Si un hilo se retrasa mientras se mantiene un bloqueo, todos los hilos que necesitan este bloqueo no podrán ejecutarse.

Inversión prioritaria (inversión prioritaria)

Durante la competencia de múltiples subprocesos, si la prioridad del subproceso bloqueado es mayor y la prioridad del subproceso que mantiene el bloqueo es menor, incluso si el subproceso con la prioridad más alta se puede ejecutar primero, aún debe esperar a que se libere el bloqueo.

Soporte de hardware para concurrencia

Al principio, los multiprocesadores simultáneos proporcionaron algunas instrucciones especiales, como: Prueba y configuración, Búsqueda e incremento, Intercambio, etc. Ahora, casi cualquier múltiples procesadores se incluyen en alguna forma de átomos - - instrucciones, tales como comparar y de intercambio (de comparación y de intercambio), las condiciones de carga / almacenamiento asociados (Cargando-Linked / tienda-condicional ). El sistema operativo y la JVM utilizan estas instrucciones para implementar bloqueos y estructuras de datos concurrentes.

El bloqueo exclusivo es una tecnología pesimista, asume el peor de los casos, es necesario asegurarse de que otros subprocesos no causen interferencias para ejecutarse correctamente.

Para operaciones detalladas, el bloqueo optimista es un método más eficiente que puede completar las operaciones de actualización sin interferencias. Este método requiere un mecanismo de verificación rápida de conflictos para determinar si hay interferencia de otros subprocesos durante el proceso de actualización. Si la hay, esta operación fallará.

Instrucción CAS

En la mayoría de las arquitecturas de procesadores se implementará una instrucción de comparación e intercambio (CAS)

CAS contiene tres operandos:

  • Necesita leer y escribir la ubicación de memoria V
  • El valor para comparar A
  • Nuevo valor por escribir B

El significado de CAS es: Creo que el valor de la posición V debería ser A, si lo es, actualice el valor de V a B, de lo contrario no se modificará y me dirá cuál es realmente el valor de V.

Versión de implementación de Java-versión informal:

public class SimulatedCAS {
    
    
    private int value;
    public synchronized int get(){
    
    
        return value;
    }
    public synchronized int compareAndSwap(int expectValue,int newValue){
    
    
        int oldValue = value;
        if(oldValue == expectValue){
    
    
            value = newValue;
        }
        return oldValue;
    }

    public synchronized boolean compareAndSet(int expectValue,int newValue){
    
    
        return expectValue == compareAndSwap(expectValue, newValue);
    }
}

CAS es una tecnología optimista, espera realizar con éxito la operación de actualización, y si otro hilo modifica la variable, CAS puede detectar el error.

Una regla práctica muy útil es: en la mayoría de los procesadores, el costo de la "ruta rápida del código" para la adquisición y liberación de cerraduras sin contención es aproximadamente el doble del costo de CAS

Cerradura JAVA y CAS

Aunque la sintaxis de bloqueo del lenguaje Java es relativamente concisa, la JVM y las tareas que deben completarse al administrar bloqueos no son simples. Es necesario atravesar una ruta de código muy compleja en la JVM cuando se implementa el bloqueo, y puede causar bloqueo a nivel del sistema operativo, suspensión de subprocesos, cambio de contexto, etc. La principal desventaja de CAS es que la persona que llama debe ocuparse activamente de los problemas de contención (reintento, reversión, abandono), mientras que el bloqueo puede resolver automáticamente el problema (bloqueo).

Es aconsejable no hacer nada cuando CAS falla. Cuando CAS falla, significa que otros subprocesos pueden haber completado la operación que desea realizar.

Soporte de Java para CAS

Después de JAVA 5.0, se introdujo la clase de variable atómica para proporcionar una operación CAS eficiente para tipos numéricos y tipos de referencia. Debajo del java.util.concurrent.atomicpaquete (por ejemplo: AtomicIntegery AtomicReferenceetc.)

Clase de variable atómica

Las variables atómicas son más granulares y ligeras que los bloqueos, y es fundamental lograr un código concurrente de alto rendimiento en varios procesadores.
Las variables atómicas se pueden utilizar como una especie de "mejor volatiletipo de variable". Proporciona la volatilemisma semántica de memoria que las variables de tipo, además de admitir operaciones de actualización atómica.

JAVA 5 añade 12 clases de variables atómicas, divididos en cuatro 标量类grupos: 更新器类, 数组类, 复合变量类,.

标量类 更新器类 数组类 复合变量类
AtomicBoolean AtomicIntegerFieldUpdater AtomicIntegerArray AtomicStampedReference
AtomicLong AtomicLongFieldUpdater AtomicLongArray AtomicMarkableReference
AtomicReference AtomicReferenceFieldUpdater AtomicReferenceArray
AtomicInteger

Si la cantidad de cálculos locales de subprocesos es pequeña, la competencia por bloqueos y variables atómicas será muy intensa.
Si la cantidad de cálculos locales de subprocesos es grande, la competencia en bloqueos y variables atómicas disminuirá.

En niveles de competencia bajos a medios, las variables atómicas pueden proporcionar una mayor escalabilidad, mientras que en una competencia de alta intensidad, los bloqueos pueden evitar la competencia de manera efectiva.

Si se puede evitar el uso del estado compartido, la sobrecarga será menor. Podemos mejorar la escalabilidad mejorando la eficiencia en el manejo de la competencia, pero solo eliminando completamente la competencia podemos lograr una verdadera escalabilidad. (Es realmente abstracto, pero a partir del código de ejemplo, podemos entender las siguientes ThreadLocalcategorías)

Algoritmo sin bloqueo

En un determinado algoritmo, la falla o suspensión de un hilo no causará la falla o suspensión de otros hilos, entonces este algoritmo se denomina algoritmo sin bloqueo.

Los algoritmos de no bloqueo se pueden utilizar en muchas estructuras de datos comunes, incluidas pilas, colas, colas de prioridad, tablas hash, etc.

Versión segura de contador sin bloqueo:

public class CasCounter {
    
    
    /**
     * 原子操作,线程安全。这是个假的 CAS 类,纯粹演示用哈
     */
    private SimulatedCAS simulatedCAS;
    /**
     * 非线程安全变量
     */
    private int temp;
    public CasCounter() {
    
    
        this.simulatedCAS = new SimulatedCAS();
    }
    public int get() {
    
    
        return simulatedCAS.get();
    }
    public int increment() {
    
    
        int value;
        do {
    
    
            value = simulatedCAS.get();
        } while (value != simulatedCAS.compareAndSwap(value, value + 1));
        return value + 1;
    }
    public void tempIncrement() {
    
    
        temp++;
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        CasCounter casCounter = new CasCounter();
        CountDownLatch count = new CountDownLatch(50);

        for (int i = 0; i < 50; i++) {
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    for (int j = 0; j < 30; j++) {
    
    
                        try {
    
    
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        casCounter.increment();

                        try {
    
    
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        casCounter.tempIncrement();
                    }
                    count.countDown();
                }
            }).start();
        }
        count.await();
        System.out.println("Thread safe final cas Counter : " + casCounter.get());
        System.out.println("Thread unsafe final temp value : " + casCounter.temp);
    }
}

Pila sin bloqueo

La clave para crear un algoritmo sin bloqueo es descubrir cómo reducir el alcance de la modificación atómica a una sola variable mientras se mantiene la consistencia de los datos.

La pila es la estructura de datos encadenados más simple: cada elemento apunta a un solo elemento, y cada elemento es referenciado por un solo elemento.

/**
 * 通过 AtomicReference 实现线程安全的入栈和出栈操作
 *
 * @param <E> 栈元素类型
 */
public class ConcurrentStack<E> {
    
    
    private final AtomicReference<Node<E>> top = new AtomicReference<>();

    /**
     * 将元素放入栈顶
     *
     * @param item 待放入的元素
     */
    public void push(E item) {
    
    
        Node<E> newHead = new Node<>(item);
        Node<E> oldHead = null;
        do {
    
    
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
    }

    /**
     * 弹出栈顶部元素
     *
     * @return 栈顶部元素,可能为 null
     */
    public E pop() {
    
    
        Node<E> oldHead;
        Node<E> newHead;
        do {
    
    
            oldHead = top.get();
            if (oldHead == null) {
    
    
                return null;
            }
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }

    /**
     * 单向链表
     *
     * @param <E> 数据类型
     */
    private static class Node<E> {
    
    
        public final E item;
        public Node<E> next;

        public Node(E item) {
    
    
            this.item = item;
        }
    }
}

Lista vinculada sin bloqueo

La cola de lista vinculada es más complicada que la pila porque requiere punteros de cabeza y cola separados. Cuando se inserta correctamente un nuevo elemento, ambos punteros deben actualizarse mediante operaciones atómicas.

Necesitamos comprender las siguientes dos habilidades:

Técnica 1

En una operación de actualización que implica varios pasos, asegúrese de que la estructura de datos esté en un estado coherente. De esta manera, cuando llega el subproceso B, si se encuentra que A está realizando una actualización, entonces el subproceso B puede saber que una operación se ha completado parcialmente y no puede iniciar inmediatamente su propia operación de actualización. Luego, B puede esperar (verificando la bandera de la cola repetidamente) hasta que A se actualice, para que los dos subprocesos no interfieran entre sí.

Técnica 2

Si B encuentra que A está modificando la estructura de datos cuando llega B, debería haber suficiente información en la estructura de datos para que B pueda completar la operación de actualización de A. Si B "ayuda" a A a completar la operación de actualización, entonces B puede realizar su propia operación sin esperar a que la operación de A se complete. Cuando A intente completar otras operaciones después de la recuperación, encontrará que B las ha completado.

por ejemplo:

public class LinkedQueue<E> {
    
    
    /**
     * 链表结构
     * next 使用 AtomicReference 来管理,用来保证原子性和线程安全
     *
     * @param <E> 数据类型
     */
    private static class Node<E> {
    
    
        final E item;
        /**
         * 通过 AtomicReference 实现指针的原子操作
         */
        final AtomicReference<Node<E>> next;

        /**
         *  Node 构造方法
         * @param item 数据元素
         * @param next 下一个节点
         */
        public Node(E item, Node<E> next) {
    
    
            this.item = item;
            this.next = new AtomicReference<>(next);
        }
    }

    /**
     * 哨兵,队列为空时,头指针(head)和尾指针(tail)都指向此处
     */
    private final Node<E> GUARD = new Node<>(null, null);
    /**
     * 头节点,初始时指向 GUARD
     */
    private final AtomicReference<Node<E>> head = new AtomicReference<>(GUARD);
    /**
     * 尾节点,初始时指向 GUARD
     */
    private final AtomicReference<Node<E>> tail = new AtomicReference<>(GUARD);

    /**
     * 将数据元素放入链表尾部
     *
     * 在插入新元素之前,将首先检查tail 指针是否处于队列中间状态,
     * 如果是,那么说明有另一个线程正在插入元素。
     *      此时线程不会等待其他线程执行完成,而是帮助他完成操作,将 tail 指针指向下一个节点。
     *      然后重复进行检查确认,直到 tail 完全处于队列尾部才开始执行自己的插入操作。
     * 如果两个线程同时插入元素,curTail.next.compareAndSet 会失败,这种情况下不会对当前数据结构造成破坏。当前线程只需重新读取tail 并再次重试。
     * 如果curTail.next.compareAndSet执行成功,那么插入操作已生效。
     * 此时 tail.compareAndSet(curTail, newNode) 会进行尾部指针的移动:
     *      如果移动失败,那么当前线程将直接返回,不需要进行重试
     *      因为另一个线程在检查 tail 时候会帮助更新。
     *
     * @param item 数据元素
     * @return true 成功
     */
    public boolean put(E item) {
    
    
        Node<E> newNode = new Node<>(item, null);
        while (true) {
    
    
            Node<E> curTail = tail.get();
            Node<E> tailNext = curTail.next.get();
            //判断下尾部节点是否出现变动
            if (curTail == tail.get()) {
    
    
                //tailNext节点为空的话,说明当前 tail 节点是有效的
                if (tailNext == null) {
    
    
                    //将新节点设置成 当前尾节点 的 next节点,此处为原子操作,失败则 while 循环重试
                    //技巧1 实现点
                    if (curTail.next.compareAndSet(null, newNode)) {
    
    
                        //将 tail 节点的指针指向 新节点
                        //此处不用担心 tail.compareAndSet 会更新失败
                        //因为当更新失败的情况下,肯定存在其他线程在操作
                        //另一个线程会进入 tailNext!=null 的情况,重新更新指针
                        tail.compareAndSet(curTail, newNode);
                        return true;
                    }
                } else {
    
    
                    //当前尾节点 的 next 不为空的话,说明链表已经被其他线程操作过了
                    //直接将 tail 的 next 指针指向下个节点
                    //技巧2 实现点
                    tail.compareAndSet(curTail, tailNext);
                }
            }
        }
    }
}

A juzgar por el código más reciente, muchas clases de herramientas en el paquete concurrente se han modificado y optimizado. Por ejemplo , la implementación interna se ha cambiado al modo de realización de la mayoría de las clases concurrentes:

        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
    
    
            try {
    
    
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
    
    
                throw new Error(e);
            }
        }
    }

Problema de ABA

La operación de CAS para el problema de ABA es realmente un dolor de cabeza, Java proporcionado AtomicStampedReferencepara evitar el problema de ABA agregando referencias al número de versión. De manera similar, AtomicMarkableReferenceuse el tipo booleano para marcar si el nodo se elimina para resolver el problema ABA.

para resumir

Los algoritmos sin bloqueo mantienen la seguridad de los subprocesos mediante primitivas de concurrencia subyacentes (como CAS en lugar de bloqueos). Estas primitivas de bajo nivel están expuestas al mundo exterior a través de clases de variables atómicas.
Los algoritmos sin bloqueo son muy difíciles de diseñar e implementar, pero generalmente proporcionan una mayor escalabilidad. En el proceso de actualización de la JVM, la principal mejora en el rendimiento de la concurrencia proviene del uso de algoritmos sin bloqueo (en la JVM que ya está en la biblioteca de la plataforma).

Supongo que te gusta

Origin blog.csdn.net/lijie2664989/article/details/105739342
Recomendado
Clasificación