Análisis en profundidad del principio AQS bajo el código fuente de ReentrantLock

Introducción a AQS

El nombre completo de AQS es AbstractQueuedSynchronizer , que es una clase abstracta que implementa internamente una lista enlazada bidireccional FIFO. Cada nodo de la lista enlazada tiene un puntero al nodo anterior y un puntero al siguiente nodo, por lo que AQS puede acceder rápidamente el predecesor y el siguiente nodo de cualquier nodo. Posteriormente, cada nodo está vinculado a un subproceso. Cuando el subproceso no puede competir por el bloqueo, se agregará al final de la cola y esperará a que se libere. Cuando el bloqueo es liberado, el subproceso en el nodo principal de la cola se liberará para competir por el bloqueo.

ReentrantLock es la clase de implementación de la interfaz Lock. Es un bloqueo de sincronización de objetos de uso común y es un bloqueo reentrante. Un bloqueo reentrante significa que después de que un subproceso adquiere un bloqueo, no es necesario bloquearlo para volver a adquirirlo. , pero está directamente asociado con un contador. Aumente el número de reingresos, para más detalles, consulte este artículo

ReentrantLock encapsula una clase interna Sync y hereda la clase abstracta AbstractQueuedSynchronizer. El principio de bloqueo de ReentrantLock se basa en Sync. Echemos un vistazo al código fuente.

// 加锁
public void lock() {
    
    
	sync.lock();
}
// 解锁
public void unlock() {
    
    
	sync.release(1);
}

Este artículo analiza el principio de implementación de AQS a partir del código fuente de ReentrantLock

Código fuente de ReentrantLock

ReentrantLock también encapsula las clases internas estáticas de dos subclases de Sync, a saber, NonfairSync y FairSync. NonfairSync literalmente significa que es un bloqueo injusto, y FairSync es un bloqueo justo. Este artículo analiza principalmente NonfairSync

  • NonfairSync.bloqueo
final void lock() {
    
    
	//通过cas操作来修改state状态,表示争抢锁的操作
    if (compareAndSetState(0, 1))	
    	// 设置当前获取到锁的线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);		// 未获取到锁的线程再次尝试获取锁
}

Este código explica brevemente

  • Primero vaya a CAS para tomar el candado, si el candado se toma con éxito
  • Guarde el hilo actual que adquirió con éxito el bloqueo
  • No se pudo apoderarse del candado, llame a adquirir para recorrer la lógica de competencia del candado

Mire compareAndSetState nuevamente

protected final boolean compareAndSetState(int expect, int update) {
    
    
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSetState utiliza el método CAS para establecer el estado en 1. El método CAS consiste en comparar si el contenido que se va a modificar es el mismo que el valor esperado (el valor esperado es el valor antes de la modificación, que puede ser modificado por otros subprocesos antes de la modificación , así que primero juzgue si es consistente con el valor esperado) El valor esperado es el mismo), si la modificación es la misma, devolverá verdadero, si no es la misma, devolverá falso si la modificación falla, esto serie de operaciones es una operación atómica. El principio de CAS se puede consultar aquí

AbstractQueuedSynchronizer encapsula algunos métodos para establecer valores de variables basados ​​en CAS. Echemos un vistazo al código fuente:

	private static final Unsafe unsafe = Unsafe.getUnsafe();
	private static final long stateOffset;
	private static final long headOffset;
	private static final long tailOffset;
	private static final long waitStatusOffset;
	private static final long nextOffset;
	
	static {
    
    
	   try {
    
    
	       stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
	       headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
	       tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
	       waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
	       nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
	   } catch (Exception ex) {
    
     throw new Error(ex); }
	}
	
	private final boolean compareAndSetHead(Node update) {
    
    
	   return unsafe.compareAndSwapObject(this, headOffset, null, update);
	}
	
	private final boolean compareAndSetTail(Node expect, Node update) {
    
    
	   return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
	}
	
	private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
    
    
	   return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
	}
	
	private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
    
    
	   return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
	}

Método CAS de Unsafe

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

El núcleo de esta parte es llamar al método de la clase Unsafe. Acerca de la clase Unsafe, la introduje en el artículo anterior sobre el análisis del principio CAS. Proporciona un método local (nativo) para operar la memoria basado en la CPU. conjunto de instrucciones. Cada método tiene cuatro parámetros de entrada. El segundo parámetro de entrada es la dirección en la memoria de varias variables miembro de AbstractQueuedSynchronizer. El proceso de implementación general de estos métodos es obtener primero el valor de la memoria de la variable a través de la dirección de la variable, y luego compárelo con el valor esperado.Si el valor es el mismo, el valor de la memoria se actualiza a medida que el valor de actualización devuelve verdadero, de lo contrario, no hace nada y devuelve falso.

Volviendo al método de bloqueo, el proceso de adquisición de un bloqueo puede entenderse como:

  • Cuando state=0, significa que no hay estado de bloqueo
  • Cuando el estado> 0, significa que un subproceso ha adquirido el bloqueo. Cualquier subproceso que lo modifica a 1 a través de CAS adquiere con éxito el bloqueo. Sin embargo, debido a que ReentrantLock permite la reentrada, cuando el mismo subproceso adquiere el bloqueo de sincronización varias veces, el estado It aumentará, por ejemplo, reingresar 5 veces, luego estado = 5. Al liberar el bloqueo, también debe liberarse 5 veces hasta que el estado = 0 otros subprocesos sean elegibles para adquirir el bloqueo.

adquirir

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

La lógica principal de este método es

  • Intente adquirir un bloqueo exclusivo a través de tryAcquire, devuelva verdadero si tiene éxito, devuelva falso si falla
  • Si tryAcquire falla, el subproceso actual se encapsulará en un nodo y se agregará al final de la cola AQS a través del método addWaiter.
  • adquirirQueued, toma Nodo como parámetro e intenta adquirir el bloqueo girando.

Node
encapsula una clase interna de Node dentro de la cola AQS para guardar hilos.Node es una estructura de datos de una lista FIFO doblemente enlazada.La característica de esta estructura es que cada estructura de datos tiene dos punteros, que apuntan al nodo sucesor del nodo respectivamente. y nodos precursores. Cada nodo en realidad está encapsulado por un subproceso. Cuando el subproceso no puede competir por el bloqueo, se encapsulará como un nodo y se agregará a la cola ASQ.

Código fuente del nodo:

static final class Node {
    
    
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1; 
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;	// 等待状态标识
        volatile Node prev;		// 前节点
        volatile Node next;		// 后节点
        volatile Thread thread;	// 竞争锁的线程
        // 存储在condition队列中的后继节点
        Node nextWaiter;
		// 是否为共享锁
        final boolean isShared() {
    
    
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
    
    
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {
    
        // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {
    
         // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) {
    
     // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

subproceso, anterior y siguiente son el subproceso actual, el nodo anterior y el siguiente nodo respectivamente, por lo que una cola de nodo puede comenzar desde cualquier nodo y atravesar de adelante hacia atrás hasta el principio y el final de la cola.

Método NonfairSync.tryAcquire

protected final boolean tryAcquire(int acquires) {
    
    
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    
    
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state值,即获取锁状态
    int c = getState();
    // state等于0表示无锁状态直接获取锁并返回
    if (c == 0) {
    
    
        if (compareAndSetState(0, acquires)) {
    
    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果是已拿到锁的线程再次获取锁则state加上1,代表该锁被重入一次
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;  锁状态 + 1 表示重入次数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 未获取到锁返回true
    return false;
}

El método tryAcquire es intentar adquirir un bloqueo exclusivo. Después de que el CAS no logra adquirir el bloqueo en el primer paso, adquiere el bloqueo nuevamente antes de agregarlo a la cola AQS. Debido a que es un bloqueo injusto, el hilo que puede tener adquirido el bloqueo antes de ser agregado a la cola AQS libera el bloqueo en este momento. En este momento, el subproceso que compite por el bloqueo detrás puede pasar a adelantarse al bloqueo antes de agregar la cola AQS, por lo que a veces el subproceso que compite por el bloqueo detrás puede prevenir el bloqueo más rápido.

Bajo el bloqueo justo, cada subproceso que no logre adquirir el bloqueo en CAS se agregará a la cola AQS en estricta conformidad con el principio de primero en entrar, primero en salir, de modo que los subprocesos que compiten por el bloqueo tomarán el bloqueo en orden estricto

Método AbstractQueuedSynchronizer.addWaiter

private Node addWaiter(Node mode) {
    
    
// 将线程封装到Node中,mode为EXCLUSIVE即为独占锁
    Node node = new Node(Thread.currentThread(), mode);
    // tail是AQS的一个属性,代表队列尾节点
    Node pred = tail;
    if (pred != null) {
    
    		// tail不为空的情况,说明队列已经被初始化即有节点数据
        node.prev = pred;	// 当前线程Node的前节点指向AQS队列尾节点
        if (compareAndSetTail(pred, node)) {
    
     	// 通过CAS方式将Node添加到AQS队列尾部
            pred.next = node;	// CAS成功则原来的AQS尾节点的后节点指向当前线程Node
            return node;
        }
    }
    enq(node);	// CAS失败或AQS队列没有节点数据则进入eq方法
    return node;
}

Todo el proceso del método addWaiter consiste en encapsular el subproceso en un nodo y agregarlo al final de la cola AQS a través de CAS.

Método AbstractQueuedSynchronizer.enq

private Node enq(final Node node) {
    
    
// 进入无限for循环,即自旋
    for (;;) {
    
    
        Node t = tail;	// 获取AQS队列尾节点
        if (t == null) {
    
     // tail为空表示AQS队列没有数据需要进行初始化
       		// 通过CAS方式初始化AQS队列即创建一个空Node作为队列头部
            if (compareAndSetHead(new Node()))	
                tail = head;  // CAS成功此时AQS队列只有一个节点,因此队列头尾都是该节点
        } else {
    
    	// 如果此时AQS队列已被初始化则将Node添加到队列尾部
            node.prev = t;	// Node节点指向AQS队列尾节点
            if (compareAndSetTail(t, node)) {
    
    	// 通过CAS方式将Node添加到AQS队列尾部
                t.next = node;	// CAS成功则原来的AQS尾节点的后节点指向当前线程Node
                return t;
            }
        }
    }
}

Todo el proceso del método eq consiste en inicializar la cola AQS. Primero, asegúrese de que la cola no tenga datos de nodo, y luego agregue el nodo de subproceso actual al encabezado de la cola a través de CAS. En este momento, la cola solo tiene un nodo, así que apunte el nodo anterior de Node a sí mismo. Y establezca la cola de la cola como Nodo por medio de CAS, y apunte el nodo posterior de la cola a Node.

En este momento, si CAS falla, otros subprocesos completaron la inicialización, luego el Nodo se agrega al final de la cola a través de CAS, y luego el nodo detrás del nodo final de la cola se apunta al Nodo. ​​Si CAS falla nuevamente en este momento, esta serie de La operación es girar hasta que la cola de la cola se agregue con éxito, luego finaliza el giro.

Método AbstractQueuedSynchronizer.acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;	// 失败标识
    try {
    
    
        boolean interrupted = false;	// 线程终端标识
        for (;;) {
    
    
            final Node p = node.predecessor();	// 获取当前线程Node的前节点
            // 如果Node的前节点为AQS头部head即Node处于队列最前端,每次只有队列最前端的Node才能后去抢占锁,直到抢占成功
            if (p == head && tryAcquire(arg)) {
    
    	
                setHead(node);	// 线程抢占锁成功则将将该线程Node从队列中移除
                p.next = null; 	// 线程Node被设为head,原来的head后节点设为null使其能被GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}
private void setHead(Node node) {
    
    
    head = node;
    node.thread = null;
    node.prev = null;
}

El proceso de todo el método adquirirQueued es más o menos el siguiente:

  1. Obtenga la anterior del nodo de subproceso actual, si la anterior es el nodo principal, entonces competirá por el bloqueo, llame al método tryAcquire para aprovechar el bloqueo
  2. Si el nodo toma con éxito el candado, establezca el nodo como cabeza y elimine el nodo de cabeza de inicialización original.
  3. Si falla la adquisición del bloqueo, se determina si el subproceso debe suspenderse de acuerdo con waitStatus.Después de suspender el subproceso, cancele la operación de adquisición del bloqueo a través de cancelAcquire

Supongo que te gusta

Origin blog.csdn.net/weixin_44947701/article/details/125191641
Recomendado
Clasificación