ReentrantReadWriteLock Análise de bloqueio de leitura e gravação

Continue a criar, acelere o crescimento! Este é o 4º dia da minha participação no "Nuggets Daily New Plan · June Update Challenge", clique para ver os detalhes do evento

Introdução aos bloqueios de leitura e gravação

Existe um cenário assim na realidade: existem operações de leitura e escrita em recursos compartilhados, e as operações de gravação não são tão frequentes quanto as operações de leitura (mais leituras e menos gravações). Quando não há operação de gravação, não há problema com vários threads lendo um recurso ao mesmo tempo, portanto, vários threads devem ter permissão para ler recursos compartilhados ao mesmo tempo (leitura e leitura podem ser simultâneas); mas se um thread quiser para gravar nesses recursos compartilhados, não deveria. Outros encadeamentos têm permissão para ler e gravar no recurso (leitura-gravação, gravação-leitura, gravação-gravação mutex). Os bloqueios de leitura/gravação podem fornecer simultaneidade e taxa de transferência melhores do que os bloqueios exclusivos em situações em que há mais leituras do que gravações.

Para este cenário, o pacote concorrente do JAVA fornece um bloqueio de leitura/gravação ReentrantReadWriteLock , que mantém internamente um par de bloqueios relacionados, um para operações somente leitura, chamados de bloqueios de leitura; outro para operações de gravação, chamados de bloqueios de gravação, descritos a seguir: Os pré-requisitos para um thread inserir um bloqueio de leitura:

  • Sem bloqueios de gravação para outros threads
  • Não há solicitação de gravação ou há uma solicitação de gravação, mas o encadeamento de chamada e o encadeamento que mantém o bloqueio são os mesmos.

Os pré-requisitos para que o encadeamento insira o bloqueio de gravação:

  • Sem bloqueios de leitura por outros threads
  • Sem bloqueios de gravação para outros threads

O bloqueio de leitura e gravação tem as três características importantes a seguir:

  • Seletividade de imparcialidade : Suporta métodos de aquisição de bloqueio injustos (padrão) e justos, e a taxa de transferência ainda é injusta em relação à justiça.
  • Reentrante: Tanto os bloqueios de leitura quanto os bloqueios de gravação suportam a reentrância de thread. Tome o thread de leitura e gravação como exemplo: depois que o thread de leitura adquirir o bloqueio de leitura, ele poderá adquirir o bloqueio de leitura novamente. Depois de adquirir o bloqueio de gravação, o encadeamento de gravação pode adquirir o bloqueio de gravação novamente e também adquirir o bloqueio de leitura.
  • Downgrade de bloqueio: seguindo a sequência de adquirir um bloqueio de gravação, adquirir um bloqueio de leitura e, finalmente, liberar o bloqueio de gravação, um bloqueio de gravação pode ser rebaixado para um bloqueio de leitura.

Depois de ler a descrição acima, você pode ficar um pouco tonto, vou dar um exemplo de uma ordem de desenvolvimento anterior para ajudar você a entender. Nossa ordem tem um conceito de ordem principal e subordem: a ordem principal é codificada como orderCode, a subordem é codificada conforme a relação subOrderCodecorrespondente é 1:N. Quando estou reembolsando, preciso pagar as contas secundárias, e as contas principais serão reembolsadas. A dimensão do reembolso do subpedido é o reembolso do pedido subOrderCodeprincipal , e a dimensão é orderCodeque pode haver simultaneidade, podemos orderCodeadicionar um bloqueio de leitura e gravação ao

  • Se for o caso do reembolso do pedido principal, se o reembolso do subpedido é mutuamente exclusivo
  • Se for o caso de um reembolso de uma subordem, ela pode realmente ser paralelizada, mas a subordem é uma subOrderCodedimensão e um subOrderCodemutex de a deve ser adicionado.

Uso de bloqueio de leitura e gravação

Como armazenar bloqueios de leitura e gravação ao mesmo tempo pode ser armazenado pelo valor de state.Os 16 bits superiores representam o bloqueio de leitura e os 16 bits inferiores representam o bloqueio de gravação. Por exemplo: 0000 0000 0000 0000 (1<<16) 0000 0000 0000 0000 Os 16 bits superiores não são 0: há um bloqueio de leitura c >>>16 Os 16 bits inferiores não são 0: há um bloqueio de gravação 5

Interface ReadWriteLock

Podemos ver que ReentranReadWriteLock possui dois bloqueios, um bloqueio de leitura e um bloqueio de gravação.imagem.png

Exemplo de uso

Operação de cache:

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();
        }
    }

}

复制代码

No exemplo acima, o Cache combina um HashMap não thread-safe como a implementação do cache e usa o bloqueio de leitura e o bloqueio de gravação do bloqueio de leitura e gravação para garantir que o Cache seja thread-safe. No método get(String key) da operação de leitura, um bloqueio de leitura precisa ser adquirido, o que impede o acesso simultâneo ao método sem bloqueio. Para a operação de escrita o método put(String key, Object value) e o método clear(), o bloqueio de escrita deve ser adquirido antecipadamente ao atualizar o HashMap. Após a aquisição do bloqueio de escrita, outras threads são bloqueadas para a aquisição do bloqueio de leitura e o bloqueio de gravação, e somente o bloqueio de gravação Após o bloqueio ser liberado, outras operações de leitura e gravação podem continuar. O cache usa bloqueios de leitura e gravação para melhorar a simultaneidade das operações de leitura e também garante a visibilidade de todas as operações de leitura e gravação para cada operação de gravação e simplifica a programação.

bloquear downgrade

O downgrade de bloqueio refere-se ao downgrade de um bloqueio de gravação para um bloqueio de leitura. Se o thread atual retém o bloqueio de gravação, o libera e, finalmente, adquire o bloqueio de leitura, esse processo segmentado não pode ser chamado de downgrade de bloqueio. O downgrade de bloqueio refere-se ao processo de manter o bloqueio de gravação (de propriedade atual), adquirir o bloqueio de leitura e liberar o bloqueio de gravação (de propriedade anterior). O downgrade de bloqueio pode nos ajudar a obter o resultado modificado do thread atual sem ser destruído por outros threads, evitando a perda de atualizações.

Exemplo de uso de downgrade de bloqueio

Como os dados não são alterados com frequência, vários threads podem processar dados simultaneamente. .

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();
    }
}
复制代码

Precauções:

  • Os bloqueios de leitura não suportam variáveis ​​de condição
  • Nenhuma atualização durante a reentrada. Não suportado: no caso de manter um bloqueio de leitura, adquirir um bloqueio de gravação resultará em uma espera permanente
  • Suporte downgrade durante a reentrada: se você mantiver um bloqueio de gravação, poderá adquirir um bloqueio de leitura

Estrutura ReentranReadWriteLock

projeto de estrutura de método

imagem.png

Ler e escrever design de estado

imagem.png

Análise de código-fonte

bloqueio de gravação

O método tryAcquireé a lógica do núcleo de bloqueio do bloqueio de gravação

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;
}
复制代码

liberação de bloqueio de gravação

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;
}
复制代码

Ler aquisição de bloqueio

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 como abaixo:

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;
        }
    }
}
复制代码

liberação do bloqueio de leitura

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;
    }
}
复制代码

Referências

Acho que você gosta

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