Análisis del código original de AQS

¿Cuál es el problema de seguridad de los subprocesos?

Varios subprocesos operan en recursos compartidos al mismo tiempo, pero la atomicidad, la visibilidad y el orden de las operaciones (en Java) no se pueden garantizar, lo que provocará problemas de seguridad en los subprocesos.

Simule un escenario de toma de boletos, ejemplo de código de problema:

package lock;

public class RobTicket {
    private static int ticket = 50;

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 50; i++) {
            new Thread(new Rob(i)).start();
        }
        Thread.sleep(1000);
    }

    static class Rob implements Runnable{

        private int number;

        public Rob(int i) {
            this.number = i;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程-"+number+"-抢到票:" + ticket--);
        }
    }
}

Los resultados de un tiempo determinado son los siguientes:

线程-3-抢到票:49
线程-2-抢到票:48
线程-1-抢到票:49
线程-5-抢到票:47
线程-8-抢到票:44
线程-7-抢到票:45
......

Verifique los resultados y descubra que diferentes subprocesos han obtenido el mismo boleto

¿Cómo resolver el problema de seguridad del hilo anterior?

  1. Utilice AtomicInteger (volátil + cas)
  2. Utilice la palabra clave sincronizada
  3. Usar candado

Aquí uso ReentrantLock para resolver este problema.

package lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class RobTicket {
    private static int ticket = 50;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 50; i++) {
            new Thread(new Rob(i)).start();
        }
        Thread.sleep(1000);
    }

    static class Rob implements Runnable{

        private int number;

        public Rob(int i) {
            this.number = i;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                System.out.println("线程-"+number+"-抢到票:" + ticket--);
            } finally {
                lock.unlock();
            }
        }
    }
}

¿Cómo se implementa la capa inferior de ReentrantLock? La respuesta es: AQS

Antes de analizar AQS, primero debe comprender el patrón del método de plantilla. Puede consultar el patrón de método de plantilla de patrones de diseño (énfasis: método de plantilla y método de proceso, que se utilizará a continuación)

AQS ocupa la mitad del paquete concurrente y muchas de las herramientas de concurrencia subyacentes son implementadas por AQS.

Método de proceso en AQS

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

Estos métodos no están implementados en AQS. Si necesita implementar un bloqueo exclusivo, puede reescribir los métodos tryAcquire, tryRelease e isHeldExclusively; si desea implementar un bloqueo compartido, puede reescribir tryAcquireShared, tryReleaseShared e isHeldExclusively. Sin prestar atención al método de plantilla en AQS, implementamos un bloqueo propio reescribiendo el método de proceso. El código es el siguiente:

package lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MyLock implements Lock {
    private Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    private class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean tryAcquire(int arg) {
            int state = getState();
            if (state!=0){
                return false;
            }
            if (compareAndSetState(0, 1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        private ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
}

La implementación es bastante simple. MyLock implementa la interfaz Lock, define una clase de implementación AQS Sync internamente, reescribe los métodos tryAcquire, tryRelease e isHeldExclusively. NewCondition es conveniente para crear ConditionObject (una clase interna no estática de AQS). Pruebe si MyLock puede resolver el problema de la seguridad ya preparada. En el código:

package lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class RobTicket3 {
    private static int ticket = 50;
    private static Lock lock = new MyLock();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 50; i++) {
            new Thread(new Rob(i)).start();
        }
        Thread.sleep(1000);
    }

    static class Rob implements Runnable{

        private int number;

        public Rob(int i) {
            this.number = i;
        }

        public void run() {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                System.out.println("线程-"+number+"-抢到票:" + ticket--);
            } finally {
                lock.unlock();
            }
        }
    }
}

线程-1-抢到票:50
线程-4-抢到票:49
线程-2-抢到票:48
线程-5-抢到票:47
线程-17-抢到票:46
线程-3-抢到票:45
线程-20-抢到票:44
线程-15-抢到票:43
线程-16-抢到票:42
.......

Al verificar los resultados, confirme que MyLock surta efecto. A continuación, debe sumergirse en el código fuente para ver la implementación del método de plantilla AQS, pero primero comprender la estructura de datos en él.

Estructura de datos en AQS

Como se muestra en la figura, se trata de una lista doblemente enlazada. Cada nodo representa un hilo. Además, hay dos punteros, head y tail, respectivamente, apuntando al head y tail de la lista enlazada, formando así un sincronizador. Cuando el subproceso no logra adquirir el bloqueo, se agrega al final de la cola de sincronización.Tenga en cuenta que el CAS está configurado y el nodo principal no usa CAS. ¿Por qué?

Porque, cuando el nodo principal adquiere el bloqueo, abandona el equipo, y el último nodo se convierte en el nuevo nodo principal, y no hay competencia. Al mismo tiempo, es posible que varios subprocesos no compitan por los bloqueos y luego sea necesario agregarlos a la cola de sincronización, por lo que es necesario utilizar CAS.

Método de plantilla en AQS

Tome la adquisición y la liberación como ejemplos, y cargue el código fuente:

Primero mira el código fuente de accquire

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • tryAcquire (int arg)

Este método de AQS se reescribe en MyLock.Sync

  1. Obtener el estado, 0 significa que ningún subproceso ha adquirido el bloqueo, 1 significa que el subproceso ha adquirido el bloqueo y volverá directamente a la falla si el subproceso ha adquirido el bloqueo.
  2. Intente establecer el valor del estado en 1, si la configuración es exitosa, el bloqueo se adquiere con éxito y el bloqueo está configurado para ser retenido por el hilo actual, y la devolución es exitosa
  • addWaiter (modo de nodo)

Cuando el subproceso no logra adquirir el bloqueo, el subproceso actual se agrega a la cola de sincronización

  1. Hablar sobre el hilo actual encapsulado en un nodo
  2. Intente establecer el hilo actual como el nodo final y finalice el método si tiene éxito; ejecute el método enq () si falla
  3. Eche un vistazo al método enq
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

El método enq es en realidad usar CAS continuamente para configurar el nodo final en un bucle sin fin 

  • adquirirQueued (nodo de nodo final, int arg)

El foco está en el bucle for. Su lógica central en este bucle sin fin es: 1. Si el nodo anterior del nodo actual es el nodo principal, intente adquirir el bloqueo, si la adquisición es exitosa, establezca el nodo actual como el nodo principal; 2., si falla la adquisición del bloqueo, el hilo actual se bloquea, sabiendo que otros hilos liberan el bloqueo

Echemos un vistazo al código fuente del lanzamiento.

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

Comparado con el proceso de bloqueo, el proceso de desbloqueo es relativamente simple

  • tryRelease (arg)

El método tryRelease de AQS se reescribe en MyLock.Sync

Establecer estado en 0

  • unparkSuccessor (nodo de nodo)

La lógica central es despertar un nodo

Atentamente, se completó el análisis de código de bloqueos exclusivos en AQS. Los bloqueos compartidos son similares, excepto que el valor del estado no es solo 0 y 1. El bloqueo es estado ++, el desbloqueo es estado--, si está interesado, estudielo usted mismo , comprobar y conocer los resultados ^ - ^

El concepto de cerradura reentrante y cerradura justa en ReentrantLock

Cuando estaba aprendiendo AQS antes, implementé una versión simplificada de MyLock, y la implementación de ReentrantLock bajo el paquete concurrente es mucho más complicada. Aquí hay una explicación de cómo los conceptos de reentrada y equidad se implementan en ReentrantLock.

  • Bloqueo reentrante: si el mismo hilo adquiere el mismo bloqueo varias veces

Esta es la implementación de tryAcquire para bloqueos exclusivos, que agrega una pieza de lógica (parte del cuadro rojo), si el bloqueo se ha mantenido, se juzga si el titular es el hilo actual, si es, estado ++, volver a adquirir el bloquear con éxito

  • Fair lock: el primer hilo que llega adquiere el bloqueo primero

Esta es la implementación de tryAcquire para bloqueos justos. Se agrega una nueva pieza de lógica (parte del cuadro rojo). Cuando el bloqueo no está retenido por un hilo, primero determine si hay un hilo en la cola de sincronización. De lo contrario, intente adquirir la cerradura para garantizar que la equidad de adquirir cerraduras

Supongo que te gusta

Origin blog.csdn.net/qq_28411869/article/details/100111984
Recomendado
Clasificación