1. Introdução ao AQS
A classe abstrata AbstractQueuedSynchronizer (doravante referida como AQS) é java.util.concurrent
o 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:
- Os recursos podem ser acessados ao mesmo tempo? Ou só pode ser acessado por um tópico ao mesmo tempo? (Função Compartilhada / Exclusiva)
- Como gerenciar simultaneamente threads que acessam recursos? (Fila de espera)
- 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 ConditionObject
implementa 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 à Unsafe
classe, 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:
- Gestão do estado de sincronização
- Bloqueando / ativando tópicos
- 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都指向空
②首个线程入队,先创建一个空的头结点,然后以自旋的方式不断尝试插入一个包含当前线程的新结点
/**
* 以自旋的方式不断尝试插入结点至队列尾部
*
* @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”即可获得源码。
完成,收工!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。