Sincronização vs Bloqueio

182

java.util.concurrent A API fornece uma classe chamada como Lock , que basicamente serializaria o controle para acessar o recurso crítico. Dá um método como park()e unpark().

Podemos fazer coisas semelhantes se pudermos usar synchronized palavras-chave wait()e notify() notifyAll()métodos e .

Gostaria de saber qual deles é melhor na prática e por quê?

sonhador
fonte
1
artigo útil aqui: javarevisited.blogspot.in/2013/03/…
roottraveller

Respostas:

178

Se você está simplesmente bloqueando um objeto, eu prefiro usar synchronized

Exemplo:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Você tem que fazer explicitamente em try{} finally{}qualquer lugar.

Enquanto que com o sincronizado, é super claro e impossível errar:

synchronized(myObject) {
    doSomethingNifty();
}

Dito isto, Locks pode ser mais útil para coisas mais complicadas, nas quais você não pode adquirir e liberar de maneira tão limpa. Sinceramente, prefiro evitar o uso de bare Locks em primeiro lugar e optar por um controle de concorrência mais sofisticado, como a CyclicBarrierou a LinkedBlockingQueue, caso atendam às suas necessidades.

Eu nunca tive um motivo para usar wait()ou, notify()mas pode haver alguns bons.

Steven Schlansker
fonte
1
Qual é a diferença entre esperar / notificar vs estacionar / desarmar do LockSupport? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
Pacerier
6
No início, o exemplo fazia sentido com fechaduras mas depois percebi que se você usar uma tentativa finalmente bloquear esse problema seria evitado com bloqueios não sendo liberado
William Reed
Ahh ... Um daqueles momentos para apreciar o modelo RAII em C ++. std::lock_guard
WhiZTiM 17/17/17
67

Gostaria de saber qual deles é melhor na prática e por quê?

Descobri que Locke Condition(e outras novas concurrentclasses) são apenas mais ferramentas para a caixa de ferramentas. Eu pude fazer quase tudo o que precisava com meu martelo de garra antigo (a synchronizedpalavra - chave), mas era difícil de usar em algumas situações. Várias dessas situações embaraçosas se tornaram muito mais simples quando adicionei mais ferramentas à minha caixa de ferramentas: um martelo de borracha, um martelo de bola, um pé de cabra e alguns socos nas unhas. No entanto , meu antigo martelo de garra ainda vê sua parte no uso.

Eu não acho que um seja realmente "melhor" que o outro, mas cada um é mais adequado para diferentes problemas. Em poucas palavras, o modelo simples e a natureza orientada ao escopo synchronizedajudam a me proteger de bugs no meu código, mas essas mesmas vantagens às vezes são obstáculos em cenários mais complexos. São esses cenários mais complexos que o pacote simultâneo foi criado para ajudar a resolver. Mas o uso dessas construções de nível superior requer um gerenciamento mais explícito e cuidadoso no código.

===

Eu acho que o JavaDoc faz um bom trabalho ao descrever a distinção entre Locke synchronized(a ênfase é minha):

As implementações de bloqueio fornecem operações de bloqueio mais abrangentes do que as obtidas por métodos e instruções sincronizados. Eles permitem uma estrutura mais flexível , podem ter propriedades bastante diferentes e podem suportar vários objetos Condition associados .

...

O uso de métodos ou instruções sincronizados fornece acesso ao bloqueio implícito do monitor associado a cada objeto, mas força toda aquisição e liberação de bloqueio a ocorrer de maneira estruturada em bloco : quando vários bloqueios são adquiridos, eles devem ser liberados na ordem oposta ; e todos os bloqueios devem ser liberados no mesmo escopo lexical em que foram adquiridos .

Enquanto o mecanismo de escopo para métodos e instruções sincronizados facilita muito a programação com bloqueios de monitor e ajuda a evitar muitos erros comuns de programação envolvendo bloqueios, há ocasiões em que você precisa trabalhar com bloqueios de uma maneira mais flexível. Por exemplo, * * alguns algoritmos * para atravessar estruturas de dados acessadas simultaneamente requerem o uso de "entrega em mão" ou "bloqueio de cadeia" : você adquire o bloqueio do nó A, depois o nó B, libera A e adquire C, libere B e adquira D e assim por diante. As implementações da interface Lock permitem o uso de tais técnicas, permitindo que um bloqueio seja adquirido e liberado em diferentes escopos , epermitindo que vários bloqueios sejam adquiridos e liberados em qualquer ordem .

Com esse aumento da flexibilidade, vem uma responsabilidade adicional . A ausência de bloqueio estruturado em bloco remove a liberação automática de bloqueios que ocorre com métodos e instruções sincronizados. Na maioria dos casos, o seguinte idioma deve ser usado:

...

Quando o bloqueio e o desbloqueio ocorrem em escopos diferentes , deve-se tomar cuidado para garantir que todo o código que é executado enquanto o bloqueio é mantido seja protegido por tentativa-final ou tentativa-captura para garantir que a trava seja liberada quando necessário.

As implementações de bloqueio fornecem funcionalidade adicional sobre o uso de métodos e instruções sincronizados, fornecendo uma tentativa sem bloqueio de adquirir um bloqueio (tryLock ()), uma tentativa de adquirir o bloqueio que pode ser interrompido (lockInterruptibly () e uma tentativa de adquirir o bloqueio que pode atingir o tempo limite (tryLock (long, TimeUnit)).

...

Bert F
fonte
23

Você pode obter tudo o que os utilitários em java.util.concurrent fazem com as primitivas de baixo nível synchronized, como volatile, ou aguardar / notificar

No entanto, a simultaneidade é complicada e a maioria das pessoas erra pelo menos algumas partes, tornando seu código incorreto ou ineficiente (ou ambos).

A API simultânea fornece uma abordagem de nível superior, que é mais fácil (e, como tal, mais segura) de usar. Em poucas palavras, você não precisa synchronized, volatile, wait, notifymais usar diretamente.

O bloqueio classe em si é no lado de nível inferior desta caixa de ferramentas, você pode nem precisar usar que direta ou (você pode usar Queuese Semaphore e outras coisas, etc, na maioria das vezes).

Thilo
fonte
2
A espera / notificação antiga simples é considerada uma primitiva de nível mais baixo que o java.util.concurrent.locks.LockSupport park / unpark, ou é o contrário?
Pacerier 8/08
@ Pacerier: Considero que ambos são de baixo nível (isto é, algo que um programador de aplicativos gostaria de evitar usar diretamente), mas certamente as partes de nível inferior do java.util.concurrency (como o pacote de bloqueios) são construídas no topo das primitivas nativas da JVM aguardam / notificam (que são ainda de nível inferior).
Thilo
2
Não, quero dizer dos 3: Thread.sleep / interrupt, Object.wait / notify, LockSupport.park / unpark, qual é o mais primitivo?
Pacerier
2
@ Thilo Não sei ao certo como você apoia sua afirmação que java.util.concurrenté mais fácil [em geral] do que os recursos de idioma ( synchronized, etc ...). Quando você usa, java.util.concurrentvocê tem o hábito de preencher lock.lock(); try { ... } finally { lock.unlock() }antes de escrever o código, enquanto que com um synchronizedvocê está basicamente bem desde o início. Apenas com base nisso, eu diria que synchronizedé mais fácil (dado que você deseja o seu comportamento) do que java.util.concurrent.locks.Lock. par 4
Iwan Aucamp
1
Não pense que você pode replicar exatamente o comportamento das classes AtomicXXX apenas com as primitivas de simultaneidade, pois elas dependem da chamada CAS nativa não disponível antes de java.util.concurrent.
Duncan Armstrong
15

Existem 4 fatores principais para você usar synchronizedou java.util.concurrent.Lock.

Nota: Bloqueio sincronizado é o que quero dizer quando digo bloqueio intrínseco.

  1. Quando o Java 5 saiu com o ReentrantLocks, eles provaram ter uma diferença notável na taxa de transferência do que no bloqueio intrínseco. Se você estiver procurando por um mecanismo de bloqueio mais rápido e estiver executando a versão 1.5, considere jucReentrantLock. O bloqueio intrínseco do Java 6 agora é comparável.

  2. O jucLock possui diferentes mecanismos de bloqueio. Bloquear interruptível - tente travar até que a rosca de bloqueio seja interrompida; bloqueio temporizado - tente bloquear por um certo período de tempo e desista se não tiver êxito; tryLock - tente bloquear, se algum outro segmento estiver segurando a trava. Isso tudo está incluído além do bloqueio simples. O bloqueio intrínseco oferece apenas bloqueio simples

  3. Estilo. Se 1 e 2 não se enquadram em categorias do que você está preocupado com a maioria das pessoas, inclusive eu, acharia os semenáticos de bloqueio intrínsecos mais fáceis de ler e menos detalhados do que os bloqueios por jucLock.
  4. Várias condições. Um objeto no qual você bloqueia só pode ser notificado e aguardado por um único caso. O método newCondition da Lock permite que um único Lock tenha várias razões para aguardar ou sinalizar. Ainda tenho que realmente precisar dessa funcionalidade na prática, mas é um recurso interessante para quem precisa.
John Vint
fonte
Gostei dos detalhes do seu comentário. Eu acrescentaria mais um ponto de bala - o ReadWriteLock fornece um comportamento útil se você estiver lidando com vários threads, apenas alguns dos quais precisam gravar no objeto. Vários segmentos podem ler simultaneamente o objeto e são bloqueados apenas se outro segmento já estiver gravando nele.
Sam Goldberg
5

Gostaria de acrescentar mais algumas coisas em cima da resposta de Bert F.

LocksOferece suporte a vários métodos para controle de bloqueio de granulação mais fina, que são mais expressivos que monitores implícitos ( synchronizedbloqueios)

Um bloqueio fornece acesso exclusivo a um recurso compartilhado: apenas um encadeamento de cada vez pode adquirir o bloqueio e todo o acesso ao recurso compartilhado exige que o bloqueio seja adquirido primeiro. No entanto, alguns bloqueios podem permitir acesso simultâneo a um recurso compartilhado, como o bloqueio de leitura de um ReadWriteLock.

Vantagens do bloqueio sobre sincronização na página de documentação

  1. O uso de métodos ou instruções sincronizados fornece acesso ao bloqueio implícito do monitor associado a cada objeto, mas força toda a aquisição e liberação de bloqueio a ocorrer de maneira estruturada em bloco

  2. As implementações de bloqueio fornecem funcionalidade adicional sobre o uso de métodos e instruções sincronizados, fornecendo uma tentativa sem bloqueio de adquirir um lock (tryLock()), uma tentativa de adquirir o bloqueio que pode ser interrompido ( lockInterruptibly()e uma tentativa de adquirir o bloqueio que pode timeout (tryLock(long, TimeUnit)).

  3. Uma classe Lock também pode fornecer um comportamento e semântica bastante diferente do bloqueio implícito do monitor, como pedidos garantidos, uso não reentrante ou detecção de deadlock

ReentrantLock : em termos simples, de acordo com meu entendimento, ReentrantLockpermite que um objeto entre novamente de uma seção crítica para outra seção crítica. Como você já tem bloqueio para inserir uma seção crítica, você pode outra seção crítica no mesmo objeto usando o bloqueio atual.

ReentrantLockprincipais recursos conforme 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.WriteLockpara adquirir ainda mais controle sobre o bloqueio granular nas operações de leitura e gravação.

Além desses três ReentrantLocks, o java 8 fornece mais um bloqueio

Carimbo:

O Java 8 é fornecido com um novo tipo de bloqueio chamado StampedLock, que também suporta bloqueios de leitura e gravação, como no exemplo acima. Ao contrário do ReadWriteLock, os métodos de bloqueio de um StampedLock retornam um carimbo representado por um valor longo.

Você pode usar esses carimbos para liberar uma trava ou para verificar se a trava ainda é válida. Os bloqueios carimbados adicionalmente oferecem suporte a outro modo de bloqueio chamado bloqueio otimista.

Dê uma olhada neste artigo sobre o uso de diferentes tipos de ReentrantLocke StampedLockfechaduras.

Ravindra babu
fonte
4

A principal diferença é a justiça, em outras palavras, os pedidos são tratados como FIFO ou pode haver barcaças? A sincronização no nível do método garante uma alocação justa ou FIFO do bloqueio. Usando

synchronized(foo) {
}

ou

lock.acquire(); .....lock.release();

não garante justiça.

Se você tiver muita disputa pelo bloqueio, poderá encontrar facilmente trocas, onde os pedidos mais recentes obtêm o bloqueio e os pedidos mais antigos ficam bloqueados. Vi casos em que 200 threads chegam em pouco tempo para um bloqueio e o segundo chega foi processado por último. Isso é bom para algumas aplicações, mas para outras é mortal.

Consulte o livro "Concorrência Java na prática" de Brian Goetz, seção 13.3 para uma discussão completa sobre este tópico.

Brian Tarbox
fonte
5
"A sincronização no nível do método garante uma alocação justa ou FIFO do bloqueio." => Sério? Você está dizendo que um método sincronizado se comporta de maneira diferente com equidade do que agrupar o conteúdo dos métodos em um bloco {} sincronizado? Acho que não, ou entendi essa frase errada ...?
Weiresr
Sim, apesar de surpreendente e contra-intuitivo, isso é correto. O livro de Goetz é a melhor explicação.
Brian Tarbox
Se você observar o código fornecido pelo @BrianTarbox, o bloco sincronizado está usando outro objeto que não seja "this" para bloquear. Em teoria, não há diferença entre um método sincronizado e colocar todo o corpo do referido método dentro de um bloco sincronizado, desde que o bloco use "isto" como bloqueio.
xburgos
A resposta deve ser editada para incluir cotação e deixar clara "garantia", aqui é "garantia estatística", não determinística.
19719 Hughes Nathan Hughes
Desculpe, acabei de descobrir que votei erroneamente esta resposta há alguns dias (clique desajeitado). Infelizmente, SO atualmente não me permite revertê-lo. Espero que eu possa consertar isso mais tarde.
Mikko Östlund
3

O livro "Java Concurrency In Practice" de Brian Goetz, seção 13.3: "... Como o ReentrantLock padrão, o bloqueio intrínseco não oferece garantias determinísticas de justiça, mas as garantias estatísticas estatísticas da maioria das implementações de bloqueio são boas o suficiente para quase todas as situações ..."

bodrin
fonte
2

O bloqueio facilita a vida dos programadores. Aqui estão algumas situações que podem ser alcançadas facilmente com o bloqueio.

  1. Bloqueie um método e libere o bloqueio em outro método.
  2. Se você tiver dois threads trabalhando em dois trechos de código diferentes, no entanto, no primeiro segmento, há um pré-requisito para um determinado trecho de código no segundo segmento (enquanto alguns outros segmentos também trabalham no mesmo trecho de código no segundo thread simultaneamente). Um bloqueio compartilhado pode resolver esse problema facilmente.
  3. Implementando monitores. Por exemplo, uma fila simples em que os métodos put e get são executados a partir de muitos outros threads. No entanto, você não deseja que vários métodos put (ou get) sejam executados simultaneamente, nem o método put and get executando simultaneamente. Um bloqueio privado torna sua vida muito mais fácil de conseguir isso.

Enquanto, o bloqueio e as condições se baseiam no mecanismo sincronizado. Portanto, certamente pode conseguir a mesma funcionalidade que você pode obter usando o bloqueio. No entanto, resolver cenários complexos com o sincronizado pode dificultar sua vida e desviar você da solução do problema real.

Md Monjur Ul Hasan
fonte
1

Diferença principal entre bloqueio e sincronizado:

  • com bloqueios, você pode liberar e adquirir os bloqueios em qualquer ordem.
  • com sincronizado, você pode liberar os bloqueios apenas na ordem em que foram adquiridos.
NKumar
fonte
0

Bloquear e sincronizar blocos serve ao mesmo propósito, mas depende do uso. Considere a parte abaixo

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

No caso acima, se um segmento entrar no bloco de sincronização, o outro bloco também estará bloqueado. Se houver vários desses blocos de sincronização no mesmo objeto, todos os blocos serão bloqueados. Nessas situações, o java.util.concurrent.Lock pode ser usado para impedir o bloqueio indesejado de blocos

Shyam
fonte