Concurrencia de Java avanzada: bloqueo exclusivo reentrante ReentrantLock detallado

Introducción al uso básico

ReentrantLock se encuentra en el paquete java.util.concurrent (JUC) y es la clase de implementación de la interfaz Lock. El uso básico es similar al sincronizado, ambos tienen las características de reentrada y exclusión mutua , pero tienen un mecanismo de bloqueo más potente y flexible. Este artículo analiza principalmente ReentrantLock desde la perspectiva del código fuente, algunos conceptos básicos y la interfaz de bloqueo pueden ser esto: Notas de lectura concurrente de Java: Lock y ReentrantLock

El uso recomendado de ReentrantLock es el siguiente:

class X {
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    //定义需要保证线程安全的方法
    public void m() {
        //加锁
        lock.lock();  
        try{
        // 保证线程安全的代码
        }
        // 使用finally块保证释放锁
        finally {
            lock.unlock()
        }
    }
}

Sistema de herencia

Concurrencia de Java avanzada: bloqueo exclusivo reentrante ReentrantLock detallado

 

  • La implementación de la interfaz de bloqueo proporciona métodos de bloqueo de teclas, como bloquear, desbloquear, tryLock, etc., así como un método para newCondition para asociar objetos de condición con el bloqueo.
  • Una sincronización se mantiene internamente, que hereda AQS, implementa el método exclusivo de adquirir y liberar recursos de sincronización proporcionados por AQS y proporciona una implementación específica de reentrada.
  • Sync tiene dos clases de implementación, que son dos implementaciones de bloqueo justo y bloqueo no justo, FairSync y NonfairSync.

Medios de bloqueo exclusivo: solo un subproceso puede adquirir el bloqueo al mismo tiempo, y otros subprocesos que adquieren el bloqueo se bloquearán y se colocarán en la cola de bloqueo AQS del bloqueo. Esta parte se puede ver: Serie de aprendizaje del código fuente del paquete concurrente de Java: la diferencia entre la adquisición y liberación de recursos compartidos y exclusivos de AQS

Método de construcción

Sync hereda directamente de AQS, y NonfairSync y FairSync heredan de Sync, realizando estrategias justas e injustas para adquirir bloqueos.

Las operaciones en ReentrantLock se delegan al objeto Sync para las operaciones reales.

/** Synchronizer providing all implementation mechanics */
    private final Sync sync;

El valor predeterminado es usar un bloqueo no equitativo: NonfairSync, puede pasar parámetros para especificar si se debe usar un bloqueo equitativo.

// 默认使用的是 非公平的策略
    public ReentrantLock() {
        sync = new NonfairSync();
    }
	// 通过fair参数指定 策略
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

estado estado

En ReentrantLock, el valor de estado de AQS representa el número de reentrada de veces que el hilo puede adquirir el bloqueo. De forma predeterminada:

  • Cuando el valor de estado es 0, significa que ningún hilo retiene el bloqueo actual.
  • Cuando el primer subproceso adquiere el bloqueo por primera vez, intentará usar CAS para establecer el valor del estado en 1. Si el CAS tiene éxito, el subproceso actual adquiere el bloqueo y luego registra que el titular del bloqueo es el subproceso actual .
  • Después de que el hilo adquiere el bloqueo por segunda vez sin liberarlo, el valor del estado se establece en 2, que es el número de reentradas.
  • Cuando el hilo libera el bloqueo, intentará usar CAS para disminuir el valor del estado en 1. Si el valor del estado es 0 después de la disminución en 1, el hilo actual libera el bloqueo .

Adquirir bloqueo

método de bloqueo de vacío ()

El método lock () de ReentrantLock se delega a la clase de sincronización, y la lógica específica se determina de acuerdo con la implementación específica de la creación de sincronización:

NonfairSync

/**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // CAS 设置获取state值
            if (compareAndSetState(0, 1))
                // 将当前线程设置为锁的持有者
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 设置失败, 调用AQS的acquire方法
                acquire(1);
        }

El estado inicial del valor de estado es 0, es decir, la operación CAS del primer subproceso se establecerá correctamente de 0 a 1, lo que indica que el subproceso actual ha adquirido el bloqueo, y luego configura el subproceso actual como el titular del bloqueo a través de setExclusiveOwnerThread método.

Si en este momento, otros subprocesos también intentan adquirir el bloqueo, el CAS falla y pasa a la lógica de adquisición.

// AQS#acquire
	public final void acquire(int arg) {
        // 调用ReentrantLock重写的tryAcquire方法
        if (!tryAcquire(arg) &&
            // tryAcquire方法返回false,则把当前线程放入AQS阻塞队列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

Oye, deberíamos tener una sensación en este momento. Dijimos cuando analizamos el método principal de AQS. AQS está diseñado en función del patrón de plantilla. El siguiente método tryAcquire está reservado para subclases. Este es el caso de NonfairSync. Realizado:

//NonfairSync#tryAcquire
        protected final boolean tryAcquire(int acquires) {
    		// 调用
            return nonfairTryAcquire(acquires);
        }

		final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取当前状态值
            int c = getState();
            // 如果当前状态值为0,如果为0表示当前锁空闲
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 看看当前的线程是不是锁的持有者
            else if (current == getExclusiveOwnerThread()) {
                // 如果是的话 将状态设置为  c + acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

Todavía es fácil de entender, primero echemos un vistazo al valor del estado de bloqueo.

  • Si es 0, CAS intenta adquirir el bloqueo, cambia el estado de 0 a 1 y establece el titular del bloqueo en el hilo actual, que es el mismo que la lógica anterior.
  • Si no es 0, significa que ha sido retenido por un hilo. ¿Ves quién tiene el candado? Si es usted mismo, es fácil de manejar, vuelva a ingresar, cambie el estado a nextc [estado original + adquisiciones entrantes] y devuelva verdadero. Note aquí: nextc <0 indica que el número de desbordamientos reentrantes.
  • Si la cerradura ya está ocupada por otros, devuelva falso y espere a que el método siguiente adquiridoQueued (addWaiter (Node.EXCLUSIVE), arg)) se coloque en la cola de bloqueo de AQS.

La injusticia aquí es que al adquirir el bloqueo, no comprueba si hay un hilo que solicitó el bloqueo antes que él mismo en la cola AQS actual, sino que adopta una estrategia de captura .

FairSync

La implementación tryAcquire de fair lock es la siguiente:

//FairSync#tryAcquire
		protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 状态值为0的时候,我去看看队列里面在我之前有没有线程在等
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 当前锁已经被当前线程持有
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

Compare las dos estrategias. No hace falta decir que el método hasQueuedPredecessors debe ser el núcleo para lograr la equidad. Echemos un vistazo:

// 如果当前线程有前驱节点就返回true。
	public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

El método: si el hilo actual tiene un nodo predecesor, devuelve verdadero. Entonces pensamos, ¿cuáles son las situaciones en las que no es un nodo predecesor?

  1. La cola esta vacia
  2. La cola no está vacía, pero el nodo del hilo actual es el primer nodo de AQS.

Después de saber esto, entenderemos el significado de la última cadena de expresiones: el primer elemento en la cola no es el hilo actual, devuelve verdadero, lo que indica que todavía hay personas en la cola antes que tú, no lo agarres, primero vienen primero Got .

La diferencia entre estrategias justas e injustas

Resumamos un poco:

El constructor de la clase Reentrant acepta un parámetro de equidad opcional fair . En este momento, hay dos opciones:

  • Regular (justo == verdadero): se garantiza que el hilo con el tiempo de espera más largo obtendrá el bloqueo primero, que en realidad es un bloqueo por orden de llegada , es decir, FIFO.
  • Injusto (justo == falso): este bloqueo no garantiza ninguna secuencia de acceso específica.

Los bloqueos justos a menudo reflejan un rendimiento general más bajo que los bloqueos injustos, es decir, más lento, porque cada vez es necesario ver si hay alguna cola en la cola. La equidad de los bloqueos no garantiza la equidad de la programación de subprocesos, pero los bloqueos justos pueden reducir la probabilidad de "inanición".

Cabe señalar que el método tryLock () irregular no admite configuraciones de equidad. Si el bloqueo está disponible, lo adquirirá con éxito incluso si otros subprocesos esperan más que él.

bloqueo anulado Interrumpidamente ()

Este método es similar al método de bloqueo, pero la diferencia es que puede responder a las interrupciones: cuando el hilo actual llama a este método, si otros hilos llaman al método interrupt () del hilo actual, el hilo actual lanzará una InterruptedException y luego regresa.

// ReentrantLock#lockInterruptibly
	public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
	// AQS#acquireInterruptibly
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 如果当前线程被中断,则直接抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取资源
        if (!tryAcquire(arg))
            // 调用AQS可被中断的方法
            doAcquireInterruptibly(arg);
    }

método booleano tryLock ()

Intente adquirir el bloqueo. Si el bloqueo no está actualmente en manos de otros subprocesos, el subproceso actual adquiere el bloqueo y devuelve verdadero, de lo contrario devuelve falso.

La lógica general es similar al método de bloqueo injusto, pero este método devolverá directamente el resultado de adquirir el bloqueo, independientemente de si es verdadero o falso, no se bloqueará.

// ReentrantLock# tryLock
	public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
	abstract static class Sync extends AbstractQueuedSynchronizer {
		// Sync#nonfairTryAcquire
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

  • El método de implementación tryLock ()  , en la implementación, espera obtener rápidamente si el bloqueo se puede adquirir, por lo que incluso si se establece en fair = true (usando fair lock), aún llame al método Sync # nonfairTryAcquire (int adquiere).
  • Si realmente desea que tryLock () se base en la equidad del bloqueo, puede llamar al método #tryLock (0, TimeUnit) para lograrlo.

boolean tryLock (tiempo de espera largo, unidad TimeUnit)

// ReentrantLock# tryLock
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	// AQS#tryAcquireNanos
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

Intente adquirir el bloqueo. Si la adquisición falla, el hilo actual se suspenderá por un tiempo especificado. Una vez transcurrido el tiempo, el hilo actual se activará. Si el bloqueo aún no se adquiere, se devolverá falso.

Además, este método responderá a las interrupciones. Si otros hilos llaman al método interrupt () del hilo actual, responde a la interrupción y lanza una excepción.

Liberar bloqueo

método de desbloqueo vacío ()

// ReentrantLock#unlock
	public void unlock() {
        sync.release(1);
    }
	//AQS# release
    public final boolean release(int arg) {
        // 子类实现tryRelease
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	abstract static class Sync extends AbstractQueuedSynchronizer {
		// Sync#tryRelease
        protected final boolean tryRelease(int releases) {
            // 计算解锁后的次数,默认减1
            int c = getState() - releases;
            // 如果想要解锁的人不是当前的锁持有者,直接抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 可重入次数为0,清空锁持有线程
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 可重入次数还没到0,只需要改变一下下state就可
            setState(c);
            return free;
        }
    }

Intenta liberar el bloqueo. Si el hilo actual mantiene el bloqueo, llamar a este método reducirá el estado de AQS en 1.

Si el estado es 0 después de restar 1, el hilo actual liberará el bloqueo.

Si el hilo actual no es el titular del bloqueo e intenta llamar a este método, se lanza una IllegalMonitorStateException.

Condición implementa productores consumidores

La condición se usa para reemplazar esperar () y notificar () en el objeto tradicional para realizar la cooperación entre subprocesos. La espera () y la señal () de la condición se utilizan para manejar la cooperación entre subprocesos para que sea más segura y eficiente .

El uso de Condition debe usarse entre lock () y unlock () , y solo se puede obtener a través de lock.newCondition (). El principio de implementación se aprenderá específicamente más adelante.

public class BlockingQueue {

    final Object[] items; // 缓冲数组
    final ReentrantLock lock = new ReentrantLock(); // 非公平独占锁
    final Condition notFull = lock.newCondition(); // 未满条件
    final Condition notEmpty = lock.newCondition(); // 未空条件
    private int putIdx; // 添加操作的指针
    private int takeIdx; // 获取操作的指针
    private int count; // 队列中元素个数

    public BlockingQueue(int capacity) {
        if(capacity < 0) throw new IllegalArgumentException();
        items = new Object[capacity];
    }

    // 插入
    public void put(Object item) throws InterruptedException {
        try {
            lock.lock(); // 上锁
            while (items.length == count) { // 满了
                notFull.await(); // 其他插入线程阻塞起来
            }
            enqueue(item); // 没满就可以入队
        } finally {
            lock.unlock(); // 不要忘记解锁
        }
    }
    private void enqueue(Object item) {
        items[putIdx] = item;
        if (++putIdx == items.length) putIdx = 0; 
        count++;
        notEmpty.signal(); // 叫醒获取的线程
    }

    // 获取
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();// 阻塞其他获取线程
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
    private Object dequeue() {
        Object x = items[takeIdx];
        items[takeIdx] = null;
        if (++takeIdx == items.length) takeIdx = 0;
        count--;
        notFull.signal(); // 叫醒其他的插入线程
        return x;
    }
}

De hecho, lo anterior es parte de la implementación de la versión reducida de ArrayBlockingQueue. Los amigos interesados ​​pueden echar un vistazo a la implementación del código fuente. El código fuente también ha realizado un procesamiento más detallado para la concurrencia.

para resumir

Bloqueo exclusivo a nivel de API : ReentrantLock es un bloqueo exclusivo reentrante implementado en la parte inferior mediante AQS . Es diferente de la semántica de bloqueo implementada en el nivel de sintaxis nativa sincronizada. ReetrantLock implementa explícitamente bloqueos de exclusión mutua a través de dos métodos, lock () y unlock ( ).

Estado y reentrada : el estado de AQS de 0 indica que el bloqueo actual está inactivo y un valor mayor que 0 indica que el bloqueo ya está ocupado y solo un subproceso puede adquirir el bloqueo a la vez. La reentrada se determina juzgando si el hilo que sostiene el bloqueo es el hilo actual, si lo es, estado + 1, cuando se libera el bloqueo, estado-1 y 0 significa liberación completa.

Estrategias justas e injustas : ReentrantLock tiene dos estrategias, justa e injusta. La diferencia radica en si comprueba si hay un nodo predecesor en el hilo actual al adquirir el bloqueo . El valor predeterminado es una estrategia de bloqueo injusta.

Abundantes extensiones de bloqueo : proporcionan bloqueo de forma interrumpida que responde a la interrupción de la adquisición de bloqueo y proporciona una respuesta rápida método tryLock y métodos de adquisición de tiempo de espera.

condition : TODO Un objeto ReentrantLock puede vincular varios objetos Condition al mismo tiempo a través de newCondition (), y las operaciones de espera y activación del hilo son más detalladas y flexibles . Volveremos a esto cuando hablemos de Condition más adelante.

Enlace original: https://www.cnblogs.com/summerday152/p/14260300.html

Si cree que este artículo es útil para usted, puede seguir mi cuenta oficial y responder a la palabra clave [Entrevista] para obtener una compilación de los puntos de conocimiento básicos de Java y un paquete de regalo para la entrevista. Hay más artículos técnicos de productos secos y materiales relacionados para compartir, ¡que todos aprendan y progresen juntos!

Supongo que te gusta

Origin blog.csdn.net/weixin_48182198/article/details/112562798
Recomendado
Clasificación