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.
Diretório de artigos
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.