Aula 13 do JUC: JUC Lock: explicação detalhada do ReentrantLock

Aula 13 do JUC: JUC Lock: explicação detalhada do ReentrantLock

Este artigo é a décima terceira palestra do JUC, bloqueio JUC: explicação detalhada do ReentrantLock. A camada inferior do bloqueio reentrante ReentrantLock é implementada por meio de AbstractQueuedSynchronizer, portanto, você deve primeiro aprender a explicação detalhada de AbstractQueuedSynchronizer no capítulo anterior.

1. Entenda as perguntas das entrevistas das principais empresas BAT

Continue com estas perguntas, que o ajudarão a compreender melhor os pontos de conhecimento relevantes.

  • O que é reentrada e o que é um bloqueio de reentrada? Que problema ele é usado para resolver? Para evitar impasses até certo ponto
  • O núcleo do ReentrantLock é AQS, então como ele é implementado, é herdado? Vamos falar sobre o relacionamento da estrutura interna de sua classe. modo exclusivo
  • Como o ReentrantLock implementa o bloqueio justo?
  • Como o ReentrantLock implementa o bloqueio injusto?
  • O ReentrantLock é implementado por padrão como um bloqueio justo ou injusto?
  • Exemplos de bloqueios justos e injustos usando ReentrantLock?
  • Comparação entre ReentrantLock e Synchronized?

2. Análise do código-fonte do ReentrantLock

2.1. Relacionamento de herança de classe

ReentrantLock implementa a interface Lock, que define operações relacionadas a bloqueio e desbloqueio, e também há um método newCondition para gerar uma condição.

public class ReentrantLock implements Lock, java.io.Serializable

2.2. Classes internas de classes

ReentrantLock tem um total de três classes internas, e as três classes internas estão intimamente relacionadas. Vejamos primeiro o relacionamento entre as três classes.
imagem

Nota : Existem três classes: Sync, NonfairSync e FairSync dentro da classe ReentrantLock.As classes NonfairSync e FairSync herdam da classe Sync e a classe Sync herda da classe abstrata AbstractQueuedSynchronizer. Vamos analisá-los um por um.

  • Sincronizar aula

O código fonte da classe Sync é o seguinte:

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    
    // 序列号
    private static final long serialVersionUID = -5179523762034025860L;
    
    // 获取锁
    abstract void lock();
    
    // 非公平方式获取
    final boolean nonfairTryAcquire(int acquires) {
    
    
        // 当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
      	// 表示没有线程正在竞争该锁
        if (c == 0) {
    
    
          	// 比较并设置状态成功,状态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;
    }
    // 实现AQS提供的拓展点
    // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
    protected final boolean tryRelease(int releases) {
    
    
        int c = getState() - releases;
      	// 当前线程不为独占线程
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException(); // 抛出异常
        // 释放标识
        boolean free = false; 
        if (c == 0) {
    
    
            free = true;
            // 已经释放,清空独占
            setExclusiveOwnerThread(null); 
        }
        // 设置标识
        setState(c); 
        return free; 
    }
    
    // 判断资源是否被当前线程占有
    protected final boolean isHeldExclusively() {
    
    
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    // 新生一个条件
    final ConditionObject newCondition() {
    
    
        return new ConditionObject();
    }

    // Methods relayed from outer class
    // 返回资源的占用线程
    final Thread getOwner() {
    
    
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    // 返回状态
    final int getHoldCount() {
    
    
        return isHeldExclusively() ? getState() : 0;
    }

    // 资源是否被占用
    final boolean isLocked() {
    
    
        return getState() != 0;
    }

    /**
     * Reconstitutes the instance from a stream (that is, deserializes it).
     */
    // 自定义反序列化逻辑
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    
    
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}  

A classe Sync possui os seguintes métodos e suas funções são as seguintes.

imagem

  • Classe NonfairSync

A classe NonfairSync herda a classe Sync, o que significa que uma estratégia injusta é usada para obter o bloqueio. Ela implementa o método de bloqueio abstrato na classe Sync. O código-fonte é o seguinte:

// 非公平锁
static final class NonfairSync extends Sync {
    
    
    // 版本号
    private static final long serialVersionUID = 7316153563782823691L;

    // 获得锁
    final void lock() {
    
    
      	// 比较并设置状态成功,状态0表示锁没有被占用
        if (compareAndSetState(0, 1))
            // 把当前线程设置独占了锁
            setExclusiveOwnerThread(Thread.currentThread());
        else // 锁已经被占用,或者set失败
            // 以独占模式获取对象,忽略中断
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
    
    
        return nonfairTryAcquire(acquires);
    }
}

Nota : Pode-se observar no código-fonte do método lock que ele tenta adquirir o bloqueio todas as vezes, mas não espera de acordo com o princípio da espera justa, permitindo que o thread que esperou mais tempo obtenha o bloqueio.

  • Classe FairSync

A classe FairSync também herda a classe Sync, o que significa que uma estratégia justa é usada para obter o bloqueio. Ela implementa o método de bloqueio abstrato na classe Sync. O código-fonte é o seguinte:

// 公平锁
static final class FairSync extends Sync {
    
    
    // 版本序列化
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
    
    
        // 以独占模式获取对象,忽略中断
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    // 尝试公平获取锁  AQS抽象类提供的拓展点
    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;
            }
        }
      	// 状态不为0,即资源已经被线程占据
        else if (current == getExclusiveOwnerThread()) {
    
    
            // 下一个状态
            int nextc = c + acquires;
            if (nextc < 0) // 超过了int的表示范围
                throw new Error("Maximum lock count exceeded");
            // 设置状态
            setState(nextc);
            return true;
        }
        return false;
    }
}

Nota : O rastreamento do código fonte do método lock mostra que quando o recurso está ocioso, ele sempre determinará primeiro se a fila de sincronização (estrutura de dados em AbstractQueuedSynchronizer) possui um thread com um tempo de espera maior. Se existir, o thread será adicionado à fila de espera.No final, o princípio do acesso justo é realizado . Entre eles, o método lock da classe FairSync é chamado a seguir, e apenas os métodos principais são fornecidos.
imagem

Nota : Pode-se observar que enquanto o recurso estiver ocupado por outros threads, o thread será adicionado ao final da fila de sincronização sem tentar obter o recurso primeiro. Essa também é a maior diferença do Nonfair. Nonfair tentará obter recursos sempre. Se o recurso for liberado neste momento, ele será obtido pelo thread atual. Isso cria um fenômeno injusto. Quando a aquisição não é bem-sucedida, ele será adicionado à fila.tail.

2.3. Atributos de classe

A sincronização da classe ReentrantLock é muito importante. A maioria das operações na classe ReentrantLock são convertidas diretamente em operações nas classes Sync e AbstractQueuedSynchronizer.

public class ReentrantLock implements Lock, java.io.Serializable {
    
    
    // 序列号
    private static final long serialVersionUID = 7373984872572414699L;    
    // 同步队列
    private final Sync sync;
}

2.4. Construtor de classe

  • Construtor do tipo ReentrantLock()

O padrão é usar uma estratégia injusta para adquirir bloqueios.

public ReentrantLock() {
    
    
    // 默认非公平策略
    sync = new NonfairSync();
}
  • Construtor do tipo ReentrantLock (booleano)

Você pode passar parâmetros para determinar se deve usar uma estratégia justa ou uma estratégia injusta. O parâmetro true indica uma estratégia justa, caso contrário, uma estratégia injusta é usada:

public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

2.5. Análise da função central

Ao analisar o código-fonte do ReentrantLock, podemos ver que suas operações são convertidas em operações no objeto Sync. Como o Sync herda AQS, ele pode basicamente ser convertido em operações no AQS . Por exemplo, a função de bloqueio de ReentrantLock é convertida em uma chamada para a função de bloqueio de Sync. Diferentes subclasses de Sync serão chamadas dependendo da estratégia adotada (como estratégia justa ou estratégia injusta).

Portanto, pode-se ver que por trás do ReentrantLock, o AQS fornece suporte para seus serviços. Como já analisamos o código-fonte principal do AQS antes, ele não é mais complicado. Vamos analisar melhor o código-fonte por meio de exemplos.

3. Análise de exemplo

3.1. Bloqueio justo

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

class MyThread extends Thread {
    
    
    private Lock lock;
    public MyThread(String name, Lock lock) {
    
    
        super(name);
        this.lock = lock;
    }
    
    public void run () {
    
    
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread() + " running");
            try {
    
    
                Thread.sleep(500);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        } finally {
    
    
            lock.unlock();
        }
    }
}

public class AbstractQueuedSynchronizerDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Lock lock = new ReentrantLock(true);
        
        MyThread t1 = new MyThread("t1", lock);        
        MyThread t2 = new MyThread("t2", lock);
        MyThread t3 = new MyThread("t3", lock);
        t1.start();
        t2.start();    
        t3.start();
    }
}

Resultados de execução (uma vez):

Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running

Nota: Este exemplo utiliza uma estratégia justa. A partir dos resultados, pode-se observar que pode existir a seguinte sequência temporal.

imagem

Nota: Primeiro, a operação de bloqueio do thread t1 -> a operação de bloqueio do thread t2 -> a operação de bloqueio do thread t3 -> a operação de desbloqueio do thread t1 -> a operação de desbloqueio do thread t2 -> o operação de desbloqueio do thread t3. Use este diagrama de tempo para analisar melhor o fluxo de trabalho do código-fonte.

  • O thread t1 executa lock.lock.A figura a seguir mostra os principais métodos na chamada de método.
    imagem

Nota: Pode-se observar no processo de chamada que o thread t1 obteve o recurso com sucesso e pode continuar a execução.

  • O thread t2 executa lock.lock.A figura a seguir mostra os principais métodos na chamada de método.

imagem

Nota: Como pode ser visto na figura acima, o resultado final é que o thread t2 será desabilitado porque LockSupport.park é chamado.

  • O thread t3 executa lock.lock.A figura a seguir mostra os principais métodos na chamada de método.

imagem
Nota: Como pode ser visto na figura acima, o resultado final é que o thread t3 será desabilitado porque LockSupport.park é chamado.

  • O thread t1 chamado lock.unlock. A figura a seguir mostra os principais métodos na chamada de método.
    imagem

Nota: Conforme mostrado na figura acima, finalmente, o status do head mudará para 0, e o thread t2 será desestacionado, ou seja, o thread t2 poderá continuar em execução. Neste momento, o thread t3 ainda está proibido.

  • T2 obtém recursos da CPU e continua em execução. Como t2 estava estacionado antes, agora ele precisa restaurar seu estado anterior. A figura a seguir mostra os principais métodos na chamada de método.

imagem

Nota: Na função setHead, head é definido para o próximo nó do cabeçalho anterior, e tanto o campo pré quanto o campo thread são definidos como nulos. Antes de adquirirQueued retornar, a fila de sincronização tem apenas dois nós.

  • t2 executa lock.unlock.A figura a seguir mostra os principais métodos na chamada de método.
    imagem

Nota: Como pode ser visto na figura acima, o thread t3 é finalmente desestacionado para que o thread t3 possa continuar a ser executado.

  • O thread t3 obtém recursos da CPU, restaura o estado anterior e continua em execução.

imagem

Explicação: O estado final alcançado é que resta apenas um nó na fila de sincronização e, exceto que o nó tenha o status 0, o restante será nulo.

  • t3 executa lock.unlock.A figura a seguir mostra os principais métodos na chamada de método.

imagem

Explicação: O estado final é igual ao estado anterior, há um nó vazio na fila e o nó principal e o nó final apontam para ele.

Para o uso de estratégia e condição justas, você pode consultar a parte de análise de exemplo de código-fonte do artigo anterior sobre AQS, portanto não entrarei em detalhes.

4. Artigos de referência

おすすめ

転載: blog.csdn.net/qq_28959087/article/details/133500873