A estrutura de bloqueios do JUC multi-threaded Java: visão geral do AQS

1. Introdução ao AQS

A classe abstrata AbstractQueuedSynchronizer (doravante referida como AQS) é java.util.concurrento núcleo de todo o pacote. No JDK 1.5, Doug Lea introduziu o pacote JUC, e a maioria dos sincronizadores no pacote foram construídos com base no AQS. A estrutura AQS fornece um conjunto de mecanismos gerais para gerenciar o estado de sincronização, bloquear / ativar threads e gerenciar filas de espera.

Os sincronizadores bem conhecidos, como ReentrantLock, CountDownLatch, CyclicBarrier, etc., realmente implementam a API exposta pela estrutura AQS por meio de classes internas para obter várias funções do sincronizador. A principal diferença entre esses sincronizadores é, na verdade, a definição do estado de sincronização.

A estrutura AQS separa uma série de questões ao construir um sincronizador. Todas as suas operações giram em torno do recurso - estado de sincronização, e resolvem os seguintes problemas para os usuários:

  1. Os recursos podem ser acessados ​​ao mesmo tempo? Ou só pode ser acessado por um tópico ao mesmo tempo? (Função Compartilhada / Exclusiva)
  2. Como gerenciar simultaneamente threads que acessam recursos? (Fila de espera)
  3. Se o thread não pode esperar por recursos, como sair da fila de espera? (Tempo limite / interrupção)

Na verdade, este é um padrão de design de método de modelo típico: a classe pai (estrutura AQS) define o esqueleto e os detalhes da operação interna, e as regras específicas são implementadas pela subclasse.
A estrutura AQS deixa uma questão remanescente para o usuário: o
que é um recurso? Como definir se o recurso pode ser acessado?

Vamos dar uma olhada na definição desse problema para vários sincronizadores comuns:

Sincronizador Definição de recursos
ReentrantLock O recurso representa um bloqueio exclusivo. Estado 0 significa que a fechadura está disponível; 1 significa ocupado; N significa o número de reentrantes
CountDownLatch O recurso representa um contador regressivo. O estado 0 significa que o contador é zerado e todos os threads podem acessar recursos; um estado N significa que o contador não é zerado e todos os threads precisam ser bloqueados.
Semáforo Os recursos representam semáforos ou tokens. O estado ≤ 0 significa que nenhum token está disponível e todos os threads precisam ser bloqueados; maior que 0 significa que o token está disponível. Cada vez que o thread adquire um token, o estado é reduzido em 1 e o thread não libera um token , e o Estado é aumentado em 1.
ReentrantReadWriteLock Os recursos representam bloqueios de leitura compartilhados e bloqueios de gravação exclusivos. O estado é logicamente dividido em dois curtos sem sinal de 16 bits, que registram o número de threads usados ​​pelo bloqueio de leitura e o número de vezes que o bloqueio de gravação é reinserido.

Em resumo, a estrutura AQS fornece as seguintes funções:

1.1 Fornece um conjunto de estrutura de modelo

Devido à existência de simultaneidade, muitas são as situações que precisam ser consideradas, portanto, é muito importante atingir esses dois objetivos de forma relativamente simples, pois para os usuários (usuários do framework AQS), muitas vezes não se importam Detalhes intrincados dentro. Na verdade, o AQS usa o padrão de método de modelo para conseguir isso. A maioria dos métodos em AQS são finais ou privados, o que significa que Doug Lea não deseja que os usuários usem esses métodos diretamente, mas apenas sobrescreve alguns dos métodos especificados pelo modelo.
O AQS permite que os usuários resolvam o problema acima mencionado " como definir se o recurso pode ser acessado " , expondo as seguintes APIs :

Método Hook Descrição
tryAcquire Acesso exclusivo (número de recursos)
tryRelease Lançamento exclusivo (número de recursos)
tryAcquireShared Aquisição compartilhada (número de recursos)
tryReleaseShared Aquisição compartilhada (número de recursos)
isHeldExclusively Status exclusivo

1.2 Interrupção de suporte, tempo limite

Lembra-se dos métodos de interrupção de bloqueio, espera limitada e tentativa de bloqueio na interface de bloqueio? A realização desses métodos é, na verdade, fornecida pelo AQS embutido.
Todos os sincronizadores que usam a estrutura AQS suportam as seguintes operações:

  • Sincronização bloqueadora e não bloqueadora (como tryLock);
  • Configuração opcional de tempo limite, para que o chamador desista de esperar;
  • Operação de bloqueio interrompível.

1.3 Suporte modo exclusivo e modo compartilhado

1.4 Condição de suporte, espera

A interface Condition pode ser considerada um substituto para os métodos wait (), Notice () e NoticeAll () da classe Obechct e é usada em conjunto com Lock.
A estrutura AQS ConditionObjectimplementa a interface Condition por meio de uma classe interna para fornecer a função de espera condicional por subclasses.

Dois, descrição do método AQS

Conforme mencionado na primeira parte deste capítulo, o AQS usa o padrão de método template, a maioria dos quais é final ou privado. Chamamos esse tipo de método de * Método Skeleton * , o que significa que esses métodos são definidos pelo próprio framework AQS. , as subcategorias não podem ser substituídas.
A seguir, uma descrição resumida de alguns dos métodos mais importantes por categoria, e os detalhes e princípios de implementação específicos serão explicados em detalhes nas partes subsequentes desta série.

2.1 Operação CAS

CAS, isto é, CompareAndSet, a implementação de operações CAS em Java é confiada a uma classe chamada UnSafe. Em relação à Unsafeclasse, esta classe será introduzida em detalhes no futuro. No momento, desde que você saiba, operações atômicas em campos pode ser alcançado através desta classe.

Nome do método Modificador Descrição
compareAndSetState final protegido CAS modificar valor do estado de sincronização
compareAndSetHead final privada CAS modifica o ponteiro principal da fila de espera
compareAndSetTail final privada CAS modifica o ponteiro da cauda da fila de espera
compareAndSetWaitStatus final estático privado CAS modifica o estado de espera do nó
compareAndSetNext final estático privado CAS modifica o próximo ponteiro do nó

2.2 A operação principal da fila de espera

Nome do método Modificador Descrição
enq privado Operação de enfileiramento
addWaiter privado Operação de enfileiramento
setHead privado Definir nó principal
unparkSuccessor privado Wake up successor node
doReleaseShared privado Liberar nó compartilhado
setHeadAndPropagate privado Defina o nó principal e propague o despertar

2.3 Obtenção de recursos

Nome do método Modificador Descrição
cancelAcquire privado Cancelar o acesso aos recursos
shouldParkAfterFailedAcquire estática privada Determine se deseja bloquear o thread de chamada atual
adquiriuQueued final Tente obter recursos, falha, tente bloquear o tópico
doAcquireInterruptibly privado Acesso exclusivo a recursos (resposta à interrupção)
doAcquireNanos privado Obtenha recursos exclusivamente (aguarde um tempo limitado)
doAcquireShared privado Acesso compartilhado a recursos
doAcquireSharedInterruptibly privado Acesso compartilhado a recursos (resposta à interrupção)
doAcquireSharedNanos privado Recursos compartilhados (esperando por um tempo limitado)
Nome do método Modificador Descrição
adquirir final pública Acesso exclusivo a recursos
adquirir de forma interrompida final pública Acesso exclusivo a recursos (resposta à interrupção)
adquirir de forma interrompida final pública Obtenha recursos exclusivamente (aguarde um tempo limitado)
adquirirShared final pública Acesso compartilhado a recursos
adquirirSharedInterruptivelmente final pública Acesso compartilhado a recursos (resposta à interrupção)
tryAcquireSharedNanos final pública Recursos compartilhados (esperando por um tempo limitado)

2.4 Liberar operações de recursos

Nome do método Modificador Descrição
lançamento final pública Libere recursos exclusivos
releaseShared final pública Liberar recursos compartilhados

3. Breve descrição do princípio AQS

Como mencionamos na primeira seção, a estrutura AQS separa uma série de questões ao construir um sincronizador. Todas as suas operações giram em torno do recurso - o estado de sincronização. Portanto, em torno do recurso, existem três questões básicas derivadas:

  1. Gestão do estado de sincronização
  2. Bloqueando / ativando tópicos
  3. Gerenciamento de fila de espera de thread

3.1 Status de sincronização

Definição do
estado de sincronização O estado de sincronização é, na verdade, um recurso. O AQS usa um único int (32 bits) para salvar o estado de sincronização e expõe as operações getState, setState e compareAndSetState para ler e atualizar esse estado.

/**
 * 同步状态.
 */
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}
/**
 * 以原子的方式更新同步状态.
 * 利用Unsafe类实现
 */
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

3.2 Bloqueio / ativação de thread

Antes do JDK 1.5, além do mecanismo de monitor integrado, não havia outra maneira de bloquear e ativar com segurança e conveniência o encadeamento atual.
Após JDK1.5, o pacote java.util.concurrent.locks fornece a classe LockSupport como uma ferramenta para bloqueio de thread e ativação.

3.3 等待队列

等待队列,是AQS框架的核心,整个框架的关键其实就是如何在并发状态下管理被阻塞的线程。
等待队列是严格的FIFO队列,是Craig,Landin和Hagersten锁(CLH锁)的一种变种,采用双向链表实现,因此也叫CLH队列。

1. 结点定义
CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。
每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。

结点状态 描述
CANCELLED 1 取消。表示后驱结点被中断或超时,需要移出队列
SIGNAL -1 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
CONDITION -2 Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了
PROPAGATE -3 传播。适用于共享模式(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
INITIAL 0 默认。新结点会处于这种状态

AQS使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,之所以这种做是因为在双向链表的结构下,这样更容易实现取消和超时功能。

next指针:用于维护队列顺序,当临界区的资源被释放时,头结点通过next指针找到队首结点。
prev指针:用于在结点(线程)被取消时,让当前结点的前驱直接指向当前结点的后驱完成出队动作。

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;

    /**
    * INITAL:      0 - 默认,新结点会处于这种状态。
    * CANCELLED:   1 - 取消,表示后续结点被中断或超时,需要移出队列;
    * SIGNAL:      -1- 发信号,表示后续结点被阻塞了;(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
    * CONDITION:   -2- Condition专用,表示当前结点在Condition队列中,因为等待某个条件而被阻塞了;
    * PROPAGATE:   -3- 传播,适用于共享模式。(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
    * 
    * waitStatus表示的是后续结点状态,这是因为AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,这样更容易实现取消和超时功能。
    */
    volatile int waitStatus;

    // 前驱指针
    volatile Node prev;

    // 后驱指针
    volatile Node next;

    // 结点所包装的线程
    volatile Thread thread;

    // Condition队列使用,存储condition队列中的后继节点
    Node nextWaiter;

    Node() {
    }

    Node(Thread thread, Node mode) { 
        this.nextWaiter = mode;
        this.thread = thread;
    }
}

2. 队列定义
对于CLH队列,当线程请求资源时,如果请求不到,会将线程包装成结点,将其挂载在队列尾部。
CLH队列的示意图如下:

①初始状态,队列head和tail都指向空

②首个线程入队,先创建一个空的头结点,然后以自旋的方式不断尝试插入一个包含当前线程的新结点

image-20210306112441026

/**
 * 以自旋的方式不断尝试插入结点至队列尾部
 *
 * @return 当前结点的前驱结点
 */
private Node enq(final Node node) {
    for (; ; ) {
        Node t = tail;
        if (t == null) { // 如果队列为空,则创建一个空的head结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

四、总结

本章简要介绍了AQS的思想和原理,读者可以参考Doug Lea的论文,进一步了解AQS。
本文参考:https://segmentfault.com/a/1190000015562787

关注公众号,输入“java-summary”即可获得源码。

完成,收工!

传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。

Acho que você gosta

Origin blog.csdn.net/wuxiaolongah/article/details/114435974
Recomendado
Clasificación