Por que usar um ReentrantLock se é possível usar sincronizado (isso)?

317

Estou tentando entender o que torna a trava na simultaneidade tão importante se alguém pode usar synchronized (this). No código fictício abaixo, eu posso fazer:

  1. sincronizou o método inteiro ou sincronizou a área vulnerável ( synchronized(this){...})
  2. OU bloqueie a área de código vulnerável com um ReentrantLock.

Código:

    private final ReentrantLock lock = new ReentrantLock(); 
    private static List<Integer> ints;

    public Integer getResult(String name) { 
        .
        .
        .
        lock.lock();
        try {
            if (ints.size()==3) {
                ints=null;
                return -9;
            }   

            for (int x=0; x<ints.size(); x++) {
                System.out.println("["+name+"] "+x+"/"+ints.size()+". values >>>>"+ints.get(x));
            }

        } finally {
            lock.unlock();
        } 
        return random;
}
adhg
fonte
1
BTW todos os bloqueios intrínsecos de java são reentrantes por natureza.
Aniket Thakur
@pongapundit, portanto synchronized(this){synchronized(this){//some code}}, não causará bloqueio. Para bloqueio intrínseco, se eles obtiverem monitor em um recurso e se desejarem novamente, poderão obtê-lo sem um bloqueio morto.
Aniket Thakur

Respostas:

474

Um ReentrantLock não é estruturado , diferentemente das synchronizedconstruções - ou seja, você não precisa usar uma estrutura de bloco para bloquear e pode até manter um bloqueio entre os métodos. Um exemplo:

private ReentrantLock lock;

public void foo() {
  ...
  lock.lock();
  ...
}

public void bar() {
  ...
  lock.unlock();
  ...
}

É impossível representar esse fluxo através de um único monitor em uma synchronizedconstrução.


Além disso, ReentrantLocksuporta pesquisa de bloqueio e espera de bloqueio interrompível que suportam tempo limite . ReentrantLocktambém oferece suporte à política de justiça configurável , permitindo um agendamento de encadeamento mais flexível.

O construtor para esta classe aceita um parâmetro de justiça opcional . Quando definido true, sob contenção, os bloqueios favorecem a concessão de acesso ao segmento de espera mais longa. Caso contrário, esse bloqueio não garante nenhuma ordem de acesso específica. Os programas que usam bloqueios justos acessados ​​por muitos encadeamentos podem exibir uma taxa de transferência geral mais baixa (ou seja, são mais lentos; geralmente muito mais lentos) do que aqueles que usam a configuração padrão, mas apresentam variações menores no tempo para obter bloqueios e garantir falta de fome. Observe, no entanto, que a justiça dos bloqueios não garante a justiça do agendamento de encadeamentos. Portanto, um dos muitos encadeamentos que usam um bloqueio justo pode obtê-lo várias vezes seguidas, enquanto outros encadeamentos ativos não estão progredindo e atualmente não estão mantendo o bloqueio. Observe também que o tempo limitetryLockO método não respeita a definição de justiça. Será bem-sucedido se o bloqueio estiver disponível, mesmo se outros threads estiverem aguardando.


ReentrantLock também pode ser mais escalável , com desempenho muito melhor em contenções mais altas. Você pode ler mais sobre isso aqui .

Esta alegação foi contestada, no entanto; veja o seguinte comentário:

No teste de bloqueio reentrante, um novo bloqueio é criado a cada vez, portanto, não há bloqueio exclusivo e os dados resultantes são inválidos. Além disso, o link da IBM não oferece código-fonte para o benchmark subjacente, portanto, é impossível caracterizar se o teste foi realizado corretamente.


Quando você deve usar ReentrantLocks? De acordo com o artigo do developerWorks ...

A resposta é bastante simples - use-a quando você realmente precisar de algo que synchronizednão seja fornecido , como esperas temporizadas de bloqueio, esperas de interrupção interrompíveis, bloqueios não estruturados em bloco, várias variáveis ​​de condição ou pesquisa de bloqueios. ReentrantLocktambém possui benefícios de escalabilidade, e você deve usá-lo se realmente tiver uma situação que contenha alta contenção, mas lembre-se de que a grande maioria dos synchronizedblocos quase nunca exibe qualquer contenção, muito menos alta contenção. Eu recomendaria o desenvolvimento com sincronização até que a sincronização se mostre inadequada, em vez de simplesmente assumir que "o desempenho será melhor" se você usarReentrantLock. Lembre-se, essas são ferramentas avançadas para usuários avançados. (E usuários verdadeiramente avançados tendem a preferir as ferramentas mais simples que podem encontrar até se convencerem de que as ferramentas simples são inadequadas.) Como sempre, faça as coisas primeiro e depois se preocupe se você precisa ou não fazê-las mais rapidamente.

oldrinb
fonte
26
O link 'conhecido por ser mais escalável' para lycog.com deve ser removido. No teste de bloqueio reentrante, um novo bloqueio é criado a cada vez, portanto, não há bloqueio exclusivo e os dados resultantes são inválidos. Além disso, o link IBM não oferece código-fonte para o benchmark subjacente, portanto, é impossível caracterizar se o teste foi realizado corretamente. Pessoalmente, eu apenas removeria toda a linha sobre escalabilidade, pois toda a reivindicação não é suportada.
Dev
2
Modifiquei o post à luz da sua resposta.
precisa saber é o seguinte
6
Se o desempenho é uma grande preocupação para você, não se esqueça de procurar uma maneira em que você NÃO precise de sincronização.
Mvcive #
2
O desempenho não faz sentido para mim. Se o bloqueio de reantrantes teria um desempenho melhor, por que o sincronismo não seria implementado apenas da mesma maneira que um bloqueio de reantrantes internamente?
precisa saber é
2
@ user2761895 o ReentrantLockPseudoRandomcódigo na ligação Lycog está usando a nova marca, fechaduras uncontended cada invocação de setSeedenext
oldrinb
14

ReentrantReadWriteLocké um bloqueio especializado, enquanto que synchronized(this)é um bloqueio de uso geral. Eles são semelhantes, mas não são iguais.

Você está certo no que poderia usar em synchronized(this)vez de, ReentrantReadWriteLockmas o oposto nem sempre é verdadeiro.

Se você quiser entender melhor o que torna ReentrantReadWriteLockespecial, procure algumas informações sobre a sincronização de encadeamento produtor-consumidor.

Em geral, você pode se lembrar de que a sincronização de método inteiro e a sincronização de uso geral (usando a synchronizedpalavra - chave) podem ser usadas na maioria dos aplicativos sem pensar muito na semântica da sincronização, mas se você precisar reduzir o desempenho do seu código, talvez seja necessário explore outros mecanismos de sincronização mais refinados ou para fins especiais.

A propósito, o uso synchronized(this)- e, em geral, o bloqueio usando uma instância de classe pública - pode ser problemático porque abre seu código para possíveis bloqueios, porque alguém que não sabe intencionalmente pode tentar bloquear seu objeto em outro lugar do programa.

Mike Dinescu
fonte
para evitar potenciais dead-locks porque alguém não conscientemente pode tentar bloquear contra o seu objeto em outro lugar no uso progame uma instância de objeto privado como Monitor de sincronização assim: public class MyLock { private final Object protectedLongLockingMonitor = new Object(); private long protectedLong = 0L; public void incrementProtectedLong() { synchronized(protectedLongLockingMonitor) { protectedLong++; } } }
sushicutta
9

Na página de documentação da oracle sobre ReentrantLock :

Um bloqueio de exclusão mútua reentrante com o mesmo comportamento básico e semântica que o bloqueio implícito do monitor acessado usando métodos e instruções sincronizados, mas com recursos estendidos.

  1. Um ReentrantLock pertence ao último bloqueio com êxito, mas ainda não o desbloqueou. Um bloqueio de chamada de thread retornará, adquirindo o bloqueio com êxito, quando o bloqueio não pertencer a outro segmento. O método retornará imediatamente se o segmento atual já possuir o bloqueio.

  2. O construtor para esta classe aceita um parâmetro de justiça opcional . Quando definido como verdadeiro, sob contenção, os bloqueios favorecem a concessão de acesso ao segmento de espera mais longa . Caso contrário, esse bloqueio não garante nenhuma ordem de acesso específica.

Principais recursos do ReentrantLock , de acordo com este artigo

  1. Capacidade de bloquear de forma interrompida.
  2. Capacidade de tempo limite enquanto aguarda o bloqueio.
  3. Poder para criar um bloqueio justo.
  4. API para obter a lista de threads em espera para bloqueio.
  5. Flexibilidade para tentar bloquear sem bloquear.

Você pode usar ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock para adquirir ainda mais controle sobre o bloqueio granular nas operações de leitura e gravação.

Veja este artigo de Benjamen sobre o uso de diferentes tipos de ReentrantLocks

Ravindra babu
fonte
2

Você pode usar bloqueios de reentrada com uma política de justiça ou tempo limite para evitar a inanição do encadeamento. Você pode aplicar uma política de justiça de encadeamento. ajudará a evitar um encadeamento aguardando para sempre chegar aos seus recursos.

private final ReentrantLock lock = new ReentrantLock(true);
//the param true turns on the fairness policy. 

A "política de justiça" escolhe o próximo encadeamento executável a ser executado. É baseado em prioridade, tempo desde a última execução, blá blá

Além disso, o Synchronize pode bloquear indefinidamente se não puder escapar do bloco. O reentrantlock pode ter o tempo limite definido.

j2emanue
fonte
1

Os bloqueios sincronizados não oferecem nenhum mecanismo de fila de espera no qual, após a execução de um encadeamento, qualquer encadeamento em paralelo possa adquirir o bloqueio. Devido ao qual o encadeamento existente no sistema e em execução por um longo período de tempo nunca tem chance de acessar o recurso compartilhado, levando à inanição.

Os bloqueios reentrantes são muito flexíveis e têm uma política de justiça na qual, se um encadeamento estiver aguardando mais tempo e após a conclusão do encadeamento atualmente em execução, podemos garantir que o encadeamento mais aguardado tenha a chance de acessar o recurso compartilhado, diminuindo o rendimento do sistema e tornando-o mais demorado.

Sumit Kapoor
fonte
1

Vamos supor que este código esteja sendo executado em um thread:

private static ReentrantLock lock = new ReentrantLock();

void accessResource() {
    lock.lock();
    if( checkSomeCondition() ) {
        accessResource();
    }
    lock.unlock();
}

Como o encadeamento possui o bloqueio, permitirá que várias chamadas sejam bloqueadas () e, portanto, reinsira o bloqueio. Isso pode ser conseguido com uma contagem de referência, para que não seja necessário adquirir o bloqueio novamente.

RonTLV
fonte
0

Uma coisa a ter em mente é:

O nome ' ReentrantLock ' fornece uma mensagem errada sobre outro mecanismo de bloqueio de que eles não são reentrantes. Isso não é verdade. O bloqueio adquirido via 'sincronizado' também é reentrante em Java.

A principal diferença é que 'sincronizado' usa bloqueio intrínseco (um que todo objeto possui) enquanto a API de bloqueio não.

verão
fonte
0

Eu acho que os métodos wait / notify / notifyAll não pertencem à classe Object, pois polui todos os objetos com métodos que raramente são usados. Eles fazem muito mais sentido em uma classe de bloqueio dedicada. Portanto, desse ponto de vista, talvez seja melhor usar uma ferramenta projetada explicitamente para o trabalho em questão - por exemplo, ReentrantLock.

Solubris
fonte