ReentrantReadWriteLock Análisis de bloqueo de lectura y escritura

¡Continúe creando, acelere el crecimiento! Este es el 4to día de mi participación en el "Nuggets Daily New Plan · June Update Challenge", haz clic para ver los detalles del evento

Introducción a los bloqueos de lectura y escritura

Existe tal escenario en la realidad: hay operaciones de lectura y escritura en recursos compartidos, y las operaciones de escritura no son tan frecuentes como las operaciones de lectura (más lecturas y menos escrituras). Cuando no hay una operación de escritura, no hay problema con varios subprocesos que leen un recurso al mismo tiempo, por lo que se debe permitir que varios subprocesos lean recursos compartidos al mismo tiempo (la lectura y la lectura pueden ser simultáneas); pero si un subproceso desea para escribir en estos recursos compartidos, no debería Otros subprocesos pueden leer y escribir en el recurso (lectura-escritura, escritura-lectura, exclusión mutua de escritura-escritura). Los bloqueos de lectura y escritura pueden proporcionar una mejor simultaneidad y rendimiento que los bloqueos exclusivos en situaciones en las que hay más lecturas que escrituras.

Para este escenario, el paquete simultáneo de JAVA proporciona un bloqueo de lectura y escritura ReentrantReadWriteLock , que internamente mantiene un par de bloqueos relacionados, uno para operaciones de solo lectura, llamados bloqueos de lectura; uno para operaciones de escritura, llamados bloqueos de escritura, descritos a continuación: Los requisitos previos para que un subproceso ingrese un bloqueo de lectura:

  • Sin bloqueos de escritura para otros subprocesos
  • No hay solicitud de escritura o hay una solicitud de escritura, pero el subproceso de llamada y el subproceso que contiene el bloqueo son los mismos.

Los requisitos previos para que el subproceso ingrese al bloqueo de escritura:

  • Sin bloqueos de lectura por otros subprocesos
  • Sin bloqueos de escritura para otros subprocesos

El bloqueo de lectura y escritura tiene las siguientes tres características importantes:

  • Selectividad de equidad : Admite métodos de adquisición de bloqueo tanto injustos (predeterminados) como justos, y el rendimiento sigue siendo injusto sobre la equidad.
  • Reentrante: tanto los bloqueos de lectura como los bloqueos de escritura admiten la reentrada de subprocesos. Tome el subproceso de lectura y escritura como ejemplo: después de que el subproceso de lectura adquiere el bloqueo de lectura, puede adquirir el bloqueo de lectura nuevamente. Después de adquirir el bloqueo de escritura, el subproceso de escritura puede adquirir el bloqueo de escritura nuevamente y también puede adquirir el bloqueo de lectura.
  • Degradación de bloqueo: siguiendo la secuencia de adquirir un bloqueo de escritura, luego adquirir un bloqueo de lectura y finalmente liberar el bloqueo de escritura, un bloqueo de escritura se puede degradar a un bloqueo de lectura.

Después de leer la descripción anterior, puede estar un poco mareado, le daré un ejemplo de una orden de desarrollo anterior para ayudarlo a comprender. Nuestra orden tiene un concepto de orden principal y suborden: la orden principal se codifica como orderCode, la suborden se codifica como la relación subOrderCodecorrespondiente es 1:N. Cuando estoy reembolsando, necesito respaldar las facturas secundarias, y las facturas principales serán reembolsadas. La dimensión del reembolso del pedido secundario es el reembolso del pedido subOrderCodeprincipal , y la dimensión es orderCodeque puede haber concurrencia, podemos orderCodeagregar un bloqueo de lectura y escritura a

  • Si es el caso del reembolso del pedido principal, si el reembolso del pedido secundario es mutuamente excluyente
  • Si se trata de un reembolso de un subpedido, en realidad se puede paralelizar, pero el subpedido es una subOrderCodedimensión y subOrderCodese debe agregar un mutex de a.

Uso de bloqueo de lectura y escritura

La forma de almacenar bloqueos de lectura y escritura al mismo tiempo se puede almacenar mediante el valor de state.Los 16 bits superiores representan el bloqueo de lectura y los 16 bits inferiores representan el bloqueo de escritura. Por ejemplo: 0000 0000 0000 0000 (1<<16) 0000 0000 0000 0000 Los 16 bits superiores no son 0: hay un bloqueo de lectura c >>>16 Los 16 bits inferiores no son 0: hay un bloqueo de escritura 5

Interfaz ReadWriteLock

Podemos ver que ReentranReadWriteLock tiene dos bloqueos, un bloqueo de lectura y un bloqueo de escritura.imagen.png

Ejemplo de uso

Operación de caché:

public class ReentrantReadWriteLockCacheTest {

    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

}

复制代码

En el ejemplo anterior, Cache combina un HashMap no seguro para subprocesos como la implementación de la memoria caché y utiliza el bloqueo de lectura y el bloqueo de escritura del bloqueo de lectura y escritura para garantizar que Cache sea seguro para subprocesos. En el método get (clave de cadena) de la operación de lectura, se debe adquirir un bloqueo de lectura, lo que impide el acceso simultáneo al método sin bloqueo. Para el método de operación de escritura put (clave de cadena, valor de objeto) y el método clear (), el bloqueo de escritura debe adquirirse con anticipación al actualizar HashMap. Después de adquirir el bloqueo de escritura, otros subprocesos se bloquean para la adquisición del bloqueo de lectura. y el bloqueo de escritura, y solo el bloqueo de escritura Después de liberar el bloqueo, pueden continuar otras operaciones de lectura y escritura. La memoria caché utiliza bloqueos de lectura y escritura para mejorar la concurrencia de las operaciones de lectura y también garantiza la visibilidad de todas las operaciones de lectura y escritura para cada operación de escritura y simplifica la programación.

bloquear degradar

La degradación de bloqueo se refiere a la degradación de un bloqueo de escritura a un bloqueo de lectura. Si el subproceso actual mantiene el bloqueo de escritura, luego lo libera y finalmente adquiere el bloqueo de lectura, este proceso segmentado no se puede llamar downgrade de bloqueo. La degradación de bloqueo se refiere al proceso de mantener el bloqueo de escritura (actualmente poseído), adquirir el bloqueo de lectura y luego liberar el bloqueo de escritura (propiedad anterior). La degradación de bloqueo puede ayudarnos a obtener el resultado modificado del subproceso actual sin ser destruido por otros subprocesos, lo que evita la pérdida de actualizaciones.

Ejemplo de uso de la degradación de bloqueo

Debido a que los datos no cambian con frecuencia, varios subprocesos pueden procesar datos al mismo tiempo. Cuando los datos cambian, si el subproceso actual percibe el cambio de datos, prepara los datos y otros subprocesos de procesamiento se bloquean hasta que el subproceso actual completa el procesamiento de datos. .

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
private volatile boolean update = false;

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // TODO 准备数据的流程(略)
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        
        // 锁降级完成,写锁降级为读锁
    }
    try {
        //TODO  使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}
复制代码

Precauciones:

  • Los bloqueos de lectura no admiten variables de condición
  • Sin actualización durante el reingreso No compatible: en el caso de mantener un bloqueo de lectura, adquirir un bloqueo de escritura resultará en una espera permanente
  • Admite la degradación durante el reingreso: si mantiene un bloqueo de escritura, puede adquirir un bloqueo de lectura

Estructura ReentranReadWriteLock

diseño de la estructura del método

imagen.png

Diseño de estado de lectura y escritura

imagen.png

Análisis de código fuente

bloqueo de escritura

El método tryAcquirees la lógica del núcleo de bloqueo del bloqueo de escritura.

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    // 获取写锁状态
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 重入
        setState(c + acquires);
        return true;
    }
    // 获取写锁
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 设置写锁 owner
    setExclusiveOwnerThread(current);
    return true;
}
复制代码

liberación de bloqueo de escritura

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
复制代码

Adquisición de bloqueo de lectura

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 首次获取读锁
        if (r == 0) {
            firstReader = current;
            // 第一个线程重入
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 重入
            firstReaderHoldCount++;
        } else {
            // 后续线程,通过 ThreadLocal 获取重入次数
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}
复制代码

fullTryAcquireSharedMétodos de la siguiente manera:

final int fullTryAcquireShared(Thread current) {
    /*
     * This code is in part redundant with that in
     * tryAcquireShared but is simpler overall by not
     * complicating tryAcquireShared with interactions between
     * retries and lazily reading hold counts.
     */
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}
复制代码

liberación del bloqueo de lectura

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}
复制代码

Referencias

Supongo que te gusta

Origin juejin.im/post/7102790951042547726
Recomendado
Clasificación