Componente de sincronização JUC-AQS de contêiner simultâneo (3)

A implementação de ReentrantLock depende da estrutura do sincronizador Java AbstractQueuedSynchronizer (AQS). AQS usa uma variável volátil inteira (estado) para manter o estado de sincronização

ReentrantLock

Existem dois tipos de bloqueios em java: sincronizados e bloqueios fornecidos em JUC.
ReentrantLock e Synchronized são bloqueios reentrantes, que são essencialmente operações de bloqueio e desbloqueio.

A diferença entre ReentrantLock e synchronized

Reentrada : Ambos os bloqueios são reentrantes, a diferença não é grande, um thread entra no bloqueio, o contador é incrementado em 1 e o bloqueio pode ser liberado quando cai para 0

Realização de bloqueio : a sincronização é realizada com base na JVM (os usuários são difíceis de ver e não conseguem entender sua realização) e o ReentrantLock é realizado pelo JDK.
Diferença de desempenho : no início, o desempenho dos dois é muito pior. Quando sincronizado introduz um bloqueio de polarização e um bloqueio leve (bloqueio de rotação), o desempenho dos dois não é muito diferente. 官方推荐synchronized(É mais fácil de escrever e na verdade está pegando emprestada a tecnologia CAS do ReentrantLock, tentando resolver o problema no modo de usuário para evitar o bloqueio de thread causado pela entrada no modo kernel )
Diferença de função:

  1. Conveniência : sincronizado é mais conveniente, é garantido pelo compilador para travar e liberar. ReentrantLock precisa liberar manualmente o bloqueio, portanto, para evitar o esquecimento de liberar manualmente o bloqueio e causar um deadlock, é melhor declarar o bloqueio de liberação finalmente.
  2. O grau de bloqueio refinado e flexível , ReentrantLock é melhor do que sincronizado

Recursos exclusivos do ReentrantLock

  1. ReentrantLock pode ser especificado como um bloqueio justo e injusto
    (bloqueio justo: o encadeamento que espera primeiro obtém o bloqueio primeiro e o padrão usa um bloqueio injusto)
    sincronizado só pode ser um bloqueio injusto
  2. Fornece uma classe de condição que pode agrupar threads que precisam ser ativados.
    synchronized Ative um thread aleatoriamente ou ative todos
  3. Fornece um mecanismo de thread que pode interromper a espera por um bloqueio, lock.lockInterruptibly () é implementado. A
    implementação de ReentrantLock é um bloqueio de rotação . Ao chamar ciclicamente a operação de auto-adição do cas, ele evita que o thread que entra no estado do kernel seja bloqueado e
    sincronizado e não se esqueça de liberar o bloqueio.

ReentrantLock adquire bloqueio de liberação de bloqueio

ReentrantLock 类 图

Fair lock vs. injusto lock

int c = getState (); // No início da aquisição do bloqueio, primeiro leia o estado da variável volátil
// Comparado com nonfairExperimenteAcquire (int adquire), a única diferença é que o bloqueio justo tem uma restrição adicional ao adquirir o estado de sincronização : hasQueuedPredecessors ()
Fair lock vs. injusto lock
O método hasQueuedPredecessors () determina se o thread atual é o primeiro na fila de sincronização. Se for, retorna verdadeiro, caso contrário, retorna falso.

   //即加入了同步队列中当前节点是否有前驱节点的判断
   //返回true,则表示有线程比当前线程更早地请求获取锁
   //因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

hasQueuedPredecessors () 方法
Liberar bloqueio de sincronização: tryRelease ()

protected final boolean tryRelease(int releases) {
    
    
            int c = getState() - releases;//读取state
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
    
    
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);// 释放锁的最后,写volatile变量state
            return free;
        }

O fair lock grava o estado da variável volátil no final da liberação do bloqueio e lê a variável volátil primeiro ao adquirir o bloqueio. int c = getState();
De acordo com a regra acontece antes de volátil (a operação de gravação de uma variável volátil ocorre antes da operação de leitura subsequente desta variável volátil), a variável compartilhada que é visível antes do encadeamento que libera o bloqueio grava a variável volátil, e o encadeamento que adquire o bloqueio lê o mesmo volátil A variável se tornará imediatamente visível para o segmento que adquiriu o bloqueio.

Semântica de memória para bloqueios justos e injustos

Para bloqueios injustos, desde que o CAS defina com sucesso o estado de sincronização, isso significa que o thread atual adquiriu o bloqueio, enquanto os bloqueios regulares são diferentes.

Quando um bloqueio justo e um bloqueio injusto são liberados, um estado de variável volátil deve ser escrito no final. Quando um
bloqueio justo é adquirido, primeiro leia a variável volátil. Quando
um bloqueio injusto é adquirido, primeiro use o CAS para atualizar a variável volátil .Esta operação tem leitura e gravação voláteis. Semântica de memória.

Como o CAS tem a semântica de memória de leitura volátil e gravação volátil ao mesmo tempo? O
compilador não 读后面reordenará nenhuma operação de memória de leitura volátil e volátil ; o
compilador não reordenará nenhuma operação de memória de gravação volátil e volátil 写前面.
Combinar essas duas condições significa que, para atingir a semântica de memória de leitura e gravação volátil ao mesmo tempo, o compilador não pode reordenar o CAS e quaisquer operações de memória antes e depois do CAS.

Resumindo: o
bloqueio justo é perceber que vários threads adquirem bloqueios na ordem em que se aplicam aos bloqueios, sincronizando a fila, percebendo assim as características de justiça.
Quando o bloqueio injusto é bloqueado, a fila de espera não é considerada e o bloqueio é tentado diretamente para obter o bloqueio, portanto, o bloqueio é obtido após a aplicação do aplicativo.

Desistir de sincronizado?

ReentrantLock não apenas possui todas as funções de synchronized, mas também possui alguns recursos que não podem ser realizados por synchronized. Em termos de desempenho, ReentrantLock não é pior do que sincronizado, então devemos desistir de usar o synchronized? A resposta é não fazer isso.

A classe de bloqueio no pacote JUC é uma ferramenta para situações avançadas e usuários avançados, a menos que você tenha um entendimento particularmente claro dos recursos avançados do Lock e tenha uma necessidade clara, ou haja evidências claras de que a sincronização se tornou escalável quando o gargalo ocorre , caso contrário, continuaremos a usar o synchronized. Comparado com essas classes de bloqueio avançadas, o synchronized ainda tem algumas vantagens, por exemplo, o synchronized não pode se esquecer de liberar o bloqueio. Além disso, quando a JVM usa o synchronized para gerenciar solicitações e liberações de bloqueio, a JVM pode incluir informações de bloqueio ao gerar dumps de encadeamento. Essas informações são muito valiosas para depuração. Elas podem identificar a origem de deadlocks e outros comportamentos anormais.

Uso de ReentrantLock

//创建锁:使用Lock对象声明,使用ReentrantLock接口创建
private final static Lock lock = new ReentrantLock();
//使用锁:在需要被加锁的方法中使用
private static void add() {
    
    
    lock.lock();//获取锁
    try {
    
    
        count++;
    } finally {
    
    
        lock.unlock();//释放锁
    }
}

Código fonte

//初始化方面:
//在new ReentrantLock的时候默认给了一个不公平锁
public ReentrantLock() {
    
    
    sync = new NonfairSync();
}
//加参数来初始化指定使用公平锁还是不公平锁
//fair=true时公平锁
public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

Método de função ReentrantLock

tryLock (): Obtenha o bloqueio apenas se o bloqueio não for mantido por outro thread no momento da chamada.
tryLock (longo tempo limite, unidade TimeUnit): Se o bloqueio não for mantido por outro encadeamento dentro de um determinado tempo e o encadeamento atual não for interrompido, o bloqueio é adquirido.
lockInterruptbily: Se o thread atual não for interrompido, o bloqueio é adquirido. Se for interrompido, uma exceção é lançada.
isLocked: Consultar se o bloqueio é mantido por qualquer segmento
isHeldByCurrentThread: Consultar se o segmento atual é mantido no estado bloqueado.
isFair: Determine se é um bloqueio justo
...

Recursos relacionados à condição:

hasQueuedThread (Thread): Consultar se o thread especificado está aguardando para adquirir este bloqueio.
hasQueuedThreads (): Consultar se algum thread está aguardando para adquirir este bloqueio.
getHoldCount (): Consultar o número de threads atuais que contêm o bloqueio, ou seja, o número de chamadas para o método Lock
...

Uso da condição

A condição pode operar a ativação do thread de forma muito flexível. O seguinte é um exemplo de thread em espera e ativação, em que a sequência de saída do log é marcada com o número de série 1234

public static void main(String[] args) {
    
    
    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition = reentrantLock.newCondition();//创建condition
    //线程1
    new Thread(() -> {
    
    
        try {
    
    
            reentrantLock.lock();
            log.info("wait signal"); // 1
            condition.await();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        log.info("get signal"); // 4
        reentrantLock.unlock();
    }).start();
    //线程2
    new Thread(() -> {
    
    
        reentrantLock.lock();
        log.info("get lock"); // 2
        try {
    
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        condition.signalAll();//发送信号
        log.info("send signal"); // 3
        reentrantLock.unlock();
    }).start();
}

Resultado de saída:
1 – esperar sinal
2 – obter bloqueio
3 – enviar sinal
4 – obter sinal

Explicação do processo de saída:
1. O encadeamento 1 chama reentrantLock.lock (), o encadeamento entra na fila de espera AQS e gera o log No. 1.
Em seguida, o método awiat é chamado, o encadeamento é removido da fila AQS, o o bloqueio é liberado e a condição é adicionada diretamente à fila de espera
3, encadeamento 2 porque o encadeamento 1 libera o bloqueio e obtém o bloqueio, ele produz o log
4, o encadeamento 2 executa condition.signalAll () para enviar o sinal e gera o log
5, o nó do encadeamento 1 na fila de condição recebe o sinal, retire-o da fila de condição e coloque-o na fila de espera do AQS. Neste momento, o encadeamento 1 não é ativado.
6. O thread 2 chama o desbloqueio para liberar o bloqueio. Como há apenas o thread 1 na fila AQS, o AQS libera o bloqueio do início ao fim para ativar o thread 1
7. O thread 1 continua a ser executado, log de saída nº 4 e execute a operação de desbloqueio.

Ler e escrever bloqueio: ReentrantReadWriteLock

O bloqueio exclusivo permite que apenas um segmento acesse ao mesmo tempo (ReentrantLock é um bloqueio exclusivo).
O bloqueio de leitura e gravação pode permitir que vários threads de leitor acessem ao mesmo tempo, mas quando o thread de gravador acessa, todos os threads de leitor e outros threads de gravador são bloqueados. O bloqueio de leitura e gravação mantém um par de bloqueios, um bloqueio de leitura e um bloqueio de gravação. Ao separar o bloqueio de leitura e o bloqueio de gravação, a simultaneidade é muito melhorada em comparação com o bloqueio exclusivo geral.

Além de garantir a visibilidade das operações de gravação para operações de leitura e a melhoria da simultaneidade , os bloqueios de leitura e gravação podem simplificar a programação de cenários de interação de leitura e gravação. Adquira o bloqueio de leitura durante as operações de leitura e adquira o bloqueio de gravação durante as operações de gravação. Quando o bloqueio de gravação é adquirido, as operações de leitura e gravação subsequentes (threads de operação de gravação não atuais) são bloqueadas . Depois que o bloqueio de gravação é liberado, todas as operações continuam a ser executadas.

cenas a serem usadas

public class LockExample3 {
    
    
    private final Map<String, Data> map = new TreeMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();//读锁
    private final Lock writeLock = lock.writeLock();//写锁
    //加读锁
    public Data get(String key) {
    
    
        readLock.lock();
        try {
    
    
            return map.get(key);
        } finally {
    
    
            readLock.unlock();
        }
    }
    //加写锁  设置key对应的value,并返回旧的value
    public Data put(String key, Data value) {
    
    
        writeLock.lock();
        try {
    
    
            return map.put(key, value);
        } finally {
    
    
            writeLock.unlock();
        }
    }

    class Data {
    
    }
}

Bill lock: StempedLock

Gravação, leitura e leitura otimista
O estado de um StempedLock é composto de duas partes: versão e modo . O método de aquisição de bloqueio retorna um número como um carimbo, que usa o status de bloqueio correspondente para indicar e controlar o acesso relacionado. O número 0 significa que nenhum bloqueio de gravação está autorizado a acessar e é dividido em [leitura pessimista, leitura otimista] no bloqueio de leitura.

Leitura otimista: leia mais e escreva menos.As pessoas otimistas pensam que a chance de ler e escrever ao mesmo tempo é muito pequena, por isso não devem ser usadas de forma pessimista 完全的读取锁定. O programa pode verificar se as alterações foram escritas após a leitura e, em seguida, tomar as medidas correspondentes.

usar

//定义
private final static StampedLock lock = new StampedLock();
//需要上锁的方法
private static void add() {
    
    
    long stamp = lock.writeLock();
    try {
    
    
        count++;
    } finally {
    
    
        lock.unlock(stamp);
    }
}

Código fonte

class Point {
    
    
        private double x, y;
        private final StampedLock sl = new StampedLock();
        void move(double deltaX, double deltaY) {
    
    
            long stamp = sl.writeLock();
            try {
    
    
                x += deltaX;
                y += deltaY;
            } finally {
    
    
                sl.unlockWrite(stamp);
            }
        }

        //下面看看乐观读锁案例
        double distanceFromOrigin() {
    
     // A read-only method
            long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
            double currentX = x, currentY = y;  //将两个字段读入本地局部变量
            if (!sl.validate(stamp)) {
    
     //检查发出乐观读锁后同时是否有其他写锁发生?
                stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
                try {
    
    
                    currentX = x; // 将两个字段读入本地局部变量
                    currentY = y; // 将两个字段读入本地局部变量
                } finally {
    
    
                    sl.unlockRead(stamp);
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }

        //下面是悲观读锁案例
        void moveIfAtOrigin(double newX, double newY) {
    
     // upgrade
            // Could instead start with optimistic, not read mode
            long stamp = sl.readLock();
            try {
    
    
                while (x == 0.0 && y == 0.0) {
    
     //循环,检查当前状态是否符合
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                    if (ws != 0L) {
    
     //这是确认转为写锁是否成功
                        stamp = ws; //如果成功 替换票据
                        x = newX; //进行状态改变
                        y = newY;  //进行状态改变
                        break;
                    } else {
    
     //如果不能成功转换为写锁
                        sl.unlockRead(stamp);  //我们显式释放读锁
                        stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
                    }
                }
            } finally {
    
    
                sl.unlock(stamp); //释放读锁或写锁
            }
        }
    }

Resumindo

sincronizado: a implementação da JVM pode ser monitorada por algumas ferramentas de monitoramento.Quando ocorrer uma exceção desconhecida, a JVM ajudará automaticamente a liberar o bloqueio.
ReetrantLock, ReetrantReadWriteLock e StempedLock são todos os bloqueios de nível de objeto. Para garantir que o bloqueio deve ser liberado, é mais seguro colocá-lo finalmente . StempedLock melhorou muito o desempenho, especialmente quando há mais e mais threads de leitura.

Como escolher uma fechadura

1. Quando houver apenas alguns concorrentes, use o sincronizado
2. Existem muitos concorrentes, mas a tendência de crescimento do segmento é previsível, use ReetrantLock

Acho que você gosta

Origin blog.csdn.net/eluanshi12/article/details/85262018
Recomendado
Clasificación