Sempre que surge uma pergunta sobre o SO sobre a sincronização Java, algumas pessoas estão ansiosas para apontar que isso synchronized(this)
deve ser evitado. Em vez disso, eles afirmam, é preferível um bloqueio em uma referência privada.
Algumas das razões apresentadas são:
- algum código maligno pode roubar seu bloqueio (muito popular, este também possui uma variante "acidentalmente")
- todos os métodos sincronizados da mesma classe usam exatamente o mesmo bloqueio, o que reduz a taxa de transferência
- você está (desnecessariamente) expondo muita informação
Outras pessoas, inclusive eu, argumentam que synchronized(this)
esse idioma é muito usado (também nas bibliotecas Java), é seguro e bem compreendido. Não deve ser evitado porque você tem um bug e não tem idéia do que está acontecendo no seu programa multithread. Em outras palavras: se for aplicável, use-o.
Estou interessado em ver alguns exemplos do mundo real (nada de foobar) em que this
é preferível evitar um bloqueio quando synchronized(this)
também faria o trabalho.
Portanto: você deve sempre evitá synchronized(this)
-lo e substituí-lo por um cadeado em uma referência particular?
Algumas informações adicionais (atualizadas conforme as respostas):
- estamos falando de sincronização de instância
- implícita (
synchronized
métodos) e forma explícita desynchronized(this)
são consideradas - se você citar Bloch ou outras autoridades sobre o assunto, não deixe de fora as partes de que não gosta (por exemplo, Java efetivo, item sobre Segurança de Thread: Normalmente, é o bloqueio na própria instância, mas há exceções).
- se você precisar de granularidade em seu bloqueio que não seja o
synchronized(this)
fornecido, entãosynchronized(this)
não será aplicável. Portanto, esse não é o problema
Respostas:
Vou cobrir cada ponto separadamente.
Estou mais preocupado acidentalmente . O que isso significa é que esse uso
this
faz parte da interface exposta da sua classe e deve ser documentado. Às vezes, a capacidade de outro código usar seu bloqueio é desejada. Isso é verdade em coisas comoCollections.synchronizedMap
(veja o javadoc).Esse é um pensamento excessivamente simplista; apenas se livrar
synchronized(this)
não vai resolver o problema. A sincronização adequada para a taxa de transferência exigirá mais reflexão.Esta é uma variante do # 1. O uso de
synchronized(this)
faz parte da sua interface. Se você não quer / precisa disso exposto, não faça.fonte
keySet()
evalues()
não são bloqueadas (suas)this
, mas a instância do mapa, que é importante para obter um comportamento consistente para todas as operações do mapa. A razão pela qual o objeto de bloqueio é fatorado em uma variável é que a subclasseSynchronizedSortedMap
precisa dele para implementar submapas que bloqueiam na instância do mapa original.Bem, em primeiro lugar, deve-se salientar que:
é semanticamente equivalente a:
qual é uma razão para não usar
synchronized(this)
. Você pode argumentar que pode fazer coisas ao redor dosynchronized(this)
quarteirão. O motivo usual é tentar evitar a verificação sincronizada, o que leva a todos os tipos de problemas de simultaneidade, especificamente o problema de bloqueio com verificação dupla , que mostra apenas o quão difícil pode ser fazer uma verificação relativamente simples discussão segura.Um bloqueio privado é um mecanismo defensivo, que nunca é uma má idéia.
Além disso, como você mencionou, os bloqueios particulares podem controlar a granularidade. Um conjunto de operações em um objeto pode não estar totalmente relacionado a outro, mas
synchronized(this)
excluirá mutuamente o acesso a todos eles.synchronized(this)
realmente não te dá nada.fonte
Enquanto você estiver usando sincronizado (isto), você está usando a instância da classe como um bloqueio em si. Isso significa que, enquanto o bloqueio é adquirido pelo encadeamento 1 , o encadeamento 2 deve esperar.
Suponha o seguinte código:
Método 1 modificando a variável de um e método 2 modificando a variável b , a alteração simultânea da mesma variável por dois segmentos deve ser evitado e que é. MAS, enquanto o thread1 modifica a e o thread2 modifica b, ele pode ser executado sem nenhuma condição de corrida.
Infelizmente, o código acima não permitirá isso, pois estamos usando a mesma referência para um bloqueio; Isso significa que os threads, mesmo que não estejam em uma condição de corrida, devem esperar e, obviamente, o código sacrifica a simultaneidade do programa.
A solução é usar 2 bloqueios diferentes para duas variáveis diferentes:
O exemplo acima usa bloqueios de granulação mais fina (2 bloqueios em vez de um ( lockA e lockB para as variáveis a e b, respectivamente) e, como resultado, permite uma melhor simultaneidade, por outro lado, tornou-se mais complexo que o primeiro exemplo ...
fonte
Embora eu concorde em não aderir cegamente às regras dogmáticas, o cenário de "roubo de fechaduras" parece tão excêntrico para você? Um encadeamento pode realmente adquirir o bloqueio do seu objeto "externamente" (
synchronized(theObject) {...}
), bloqueando outros encadeamentos que aguardam métodos de instância sincronizados.Se você não acredita em código malicioso, considere que esse código pode vir de terceiros (por exemplo, se você desenvolver algum tipo de servidor de aplicativos).
A versão "acidental" parece menos provável, mas, como se costuma dizer, "faça algo à prova de idiotas e alguém inventará um idiota melhor".
Então, eu concordo com a escola de pensamento "depende do que a classe faz".
Edite os 3 primeiros comentários do eljenso a seguir:
Eu nunca experimentei o problema de roubo de fechadura, mas aqui está um cenário imaginário:
Digamos que seu sistema seja um contêiner de servlet e o objeto que estamos considerando é a
ServletContext
implementação. SeugetAttribute
método deve ser seguro para threads, pois os atributos de contexto são dados compartilhados; então você declara comosynchronized
. Vamos imaginar também que você forneça um serviço de hospedagem pública com base na sua implementação de contêiner.Sou seu cliente e implanto meu servlet "bom" em seu site. Acontece que meu código contém uma chamada para
getAttribute
.Um hacker, disfarçado de outro cliente, implanta seu servlet malicioso no seu site. Ele contém o seguinte código no
init
método:Supondo que compartilhamos o mesmo contexto de servlet (permitido pelas especificações, desde que os dois servlets estejam no mesmo host virtual), minha ligação
getAttribute
será bloqueada para sempre. O hacker conseguiu um DoS no meu servlet.Esse ataque não é possível se
getAttribute
estiver sincronizado em um bloqueio privado, porque o código de terceiros não pode adquiri-lo.Admito que o exemplo é artificial e uma visão simplista de como funciona um contêiner de servlet, mas IMHO prova isso.
Portanto, eu faria minha escolha de design com base na consideração de segurança: terei controle total sobre o código que tem acesso às instâncias? Qual seria a conseqüência de um thread manter um bloqueio em uma instância indefinidamente?
fonte
Depende da situação.
Se houver apenas uma entidade compartilhada ou mais de uma.
Veja o exemplo de trabalho completo aqui
Uma pequena introdução.
Threads e entidades compartilháveis
É possível que vários threads acessem a mesma entidade, por exemplo, vários connectionThreads compartilhando um único messageQueue. Como os threads são executados simultaneamente, pode haver uma chance de substituir os dados de alguém por outro, o que pode ser uma situação complicada.
Portanto, precisamos de uma maneira de garantir que a entidade compartilhável seja acessada apenas por um encadeamento por vez. (CONCURRÊNCIA).
Bloco
sincronizado O bloco sincronizado () é uma maneira de garantir o acesso simultâneo da entidade compartilhável.
Primeiro, uma pequena analogia
Suponha que haja duas pessoas P1, P2 (threads) um lavatório (entidade compartilhável) dentro de um banheiro e haja uma porta (trava).
Agora queremos que uma pessoa use o lavatório de cada vez.
Uma abordagem é trancar a porta por P1 quando a porta estiver trancada. P2 espera até que p1 conclua seu trabalho.
P1 destranca a porta
e somente p1 pode usar o lavatório.
sintaxe.
"this" forneceu o bloqueio intrínseco associado à classe (o desenvolvedor Java projetou a classe Object de maneira que cada objeto possa funcionar como monitor). A abordagem acima funciona bem quando há apenas uma entidade compartilhada e vários threads (1: N). N entidades compartilháveis - segmentos M Agora pense em uma situação em que há dois lavatórios dentro de um banheiro e apenas uma porta. Se estivermos usando a abordagem anterior, apenas p1 poderá usar um lavatório por vez, enquanto p2 esperará do lado de fora. É desperdício de recursos, pois ninguém está usando B2 (lavatório). Uma abordagem mais sensata seria criar uma sala menor dentro do banheiro e fornecer a eles uma porta por lavatório. Dessa forma, P1 pode acessar B1 e P2 pode acessar B2 e vice-versa.
Veja mais sobre Threads ----> aqui
fonte
Parece haver um consenso diferente nos campos C # e Java sobre isso. A maioria do código Java que eu vi usa:
considerando que a maioria do código C # opta pelo discutivelmente mais seguro:
O idioma C # é certamente mais seguro. Como mencionado anteriormente, nenhum acesso malicioso / acidental ao bloqueio pode ser feito de fora da instância. O código Java também tem esse risco, mas parece que a comunidade Java gravitou ao longo do tempo para uma versão um pouco menos segura, mas um pouco mais concisa.
Isso não é uma brincadeira com o Java, apenas um reflexo da minha experiência trabalhando nas duas linguagens.
fonte
O
java.util.concurrent
pacote reduziu bastante a complexidade do meu código de segurança de threads. Eu só tenho evidências anedóticas para prosseguir, mas a maioria dos trabalhos com os quais já visynchronized(x)
parece estar reimplementando um bloqueio, semáforo ou trava, mas usando os monitores de nível inferior.Com isso em mente, sincronizar usando qualquer um desses mecanismos é análogo à sincronização em um objeto interno, em vez de vazar uma trava. Isso é benéfico, pois você tem certeza absoluta de que controla a entrada no monitor por dois ou mais encadeamentos.
fonte
final
variáveis)Lock
API granular ]Código de amostra para usar
ReentrantLock
qual implementa aLock
interfaceVantagens do bloqueio sobre sincronizado (isso)
O uso de métodos ou instruções sincronizados força toda a aquisição e liberação de bloqueio a ocorrer de maneira estruturada em bloco.
As implementações de bloqueio fornecem funcionalidade adicional sobre o uso de métodos e instruções sincronizados, fornecendo
tryLock()
)lockInterruptibly()
)tryLock(long, TimeUnit)
).Uma classe Lock também pode fornecer comportamentos e semânticas bastante diferentes das do bloqueio implícito do monitor, como
Dê uma olhada nesta pergunta SE referente a vários tipos de
Locks
:Sincronização vs Bloqueio
Você pode obter segurança de encadeamento usando API de simultaneidade avançada em vez de blocos sincronizados. Esta página de documentação fornece boas construções de programação para garantir a segurança do encadeamento.
Os objetos de bloqueio oferecem suporte a idiomas de bloqueio que simplificam muitos aplicativos simultâneos.
Os executivos definem uma API de alto nível para iniciar e gerenciar threads. As implementações de executores fornecidas pelo java.util.concurrent fornecem gerenciamento de conjunto de encadeamentos adequado para aplicativos de grande escala.
Coleções simultâneas facilitam o gerenciamento de grandes coleções de dados e podem reduzir bastante a necessidade de sincronização.
As variáveis atômicas possuem recursos que minimizam a sincronização e ajudam a evitar erros de consistência de memória.
O ThreadLocalRandom (no JDK 7) fornece geração eficiente de números pseudo-aleatórios a partir de vários encadeamentos.
Consulte java.util.concurrent e java.util.concurrent.atomic pacotes também para outras construções de programação.
fonte
Se você decidiu isso:
então não vejo o tabu sobre o sincronizezd (isso).
Algumas pessoas usam deliberadamente sincronizado (isso) (em vez de marcar o método como sincronizado) dentro de todo o conteúdo de um método, porque acham que é "mais claro para o leitor" em qual objeto está realmente sendo sincronizado. Desde que as pessoas façam uma escolha informada (por exemplo, entendam que, ao fazer isso, estão realmente inserindo bytecodes extras no método e isso pode ter um efeito indireto em possíveis otimizações), não vejo particularmente nenhum problema com isso. . Você sempre deve documentar o comportamento simultâneo do seu programa, para que eu não veja o argumento "'sincronizado' publica o comportamento" como sendo tão atraente.
Quanto à questão de qual bloqueio de objeto você deve usar, acho que não há nada errado em sincronizar o objeto atual, se isso seria esperado pela lógica do que você está fazendo e como sua classe normalmente seria usada . Por exemplo, com uma coleção, o objeto que você esperaria logicamente bloquear geralmente é a própria coleção.
fonte
Acho que há uma boa explicação sobre por que cada uma dessas técnicas é vital em seu livro chamado Java Concurrency In Practice, de Brian Goetz. Ele deixa um ponto muito claro - você deve usar o mesmo bloqueio "EM TODA PARTE" para proteger o estado do seu objeto. O método sincronizado e a sincronização em um objeto geralmente andam de mãos dadas. Por exemplo, o vetor sincroniza todos os seus métodos. Se você tem um identificador para um objeto de vetor e vai "colocar se estiver ausente", apenas o Vector sincronizar seus próprios métodos individuais não o protegerá da corrupção de estado. Você precisa sincronizar usando sincronizado (vectorHandle). Isso resultará na aquisição do mesmo bloqueio por cada thread que possui um identificador para o vetor e protegerá o estado geral do vetor. Isso é chamado de bloqueio do lado do cliente. De fato, sabemos que o vetor sincroniza (isso) / sincroniza todos os seus métodos e, portanto, a sincronização no objeto vectorHandle resultará na sincronização adequada do estado dos objetos vetoriais. É tolice acreditar que você é seguro para threads apenas porque está usando uma coleção segura de threads. Essa é precisamente a razão pela qual o ConcurrentHashMap introduziu explicitamente o método putIfAbsent - para tornar essas operações atômicas.
Em suma
fonte
Não, você não deveria sempre . No entanto, tenho a tendência de evitá-lo quando há várias preocupações sobre um objeto em particular que só precisam ser seguras em relação a si mesmas. Por exemplo, você pode ter um objeto de dados mutáveis que possui campos "label" e "parent"; eles precisam ser seguros para threads, mas alterar um não precisa impedir que o outro seja gravado / lido. (Na prática, eu evitaria isso declarando os campos voláteis e / ou usando os wrappers AtomicFoo do java.util.concurrent).
A sincronização, em geral, é um pouco desajeitada, pois bloqueia um grande bloqueio, em vez de pensar exatamente como os threads podem trabalhar entre si. Usar
synchronized(this)
é ainda mais desajeitado e anti-social, pois está dizendo "ninguém pode mudar nada nesta classe enquanto eu seguro a fechadura". Quantas vezes você realmente precisa fazer isso?Eu preferiria ter mais bloqueios granulares; mesmo que você queira impedir que tudo mude (talvez esteja serializando o objeto), você pode adquirir todos os bloqueios para obter a mesma coisa, além de ser mais explícito dessa maneira. Quando você usa
synchronized(this)
, não está claro exatamente por que você está sincronizando ou quais são os efeitos colaterais. Se você usasynchronized(labelMonitor)
, ou melhor aindalabelLock.getWriteLock().lock()
, fica claro o que está fazendo e a que se limitam os efeitos da sua seção crítica.fonte
Resposta curta : você precisa entender a diferença e fazer escolhas, dependendo do código.
Resposta longa : Em geral, prefiro evitar a sincronização (isso) para reduzir a contenção, mas os bloqueios particulares aumentam a complexidade da qual você precisa estar ciente. Portanto, use a sincronização correta para o trabalho certo. Se você não tem tanta experiência com programação multithread, prefiro ficar com o bloqueio de instância e ler sobre este tópico. (Dito isso: apenas o uso de sincronização (isso) não torna sua classe totalmente segura para threads). Este não é um tópico fácil, mas depois que você se acostuma, a resposta para usar a sincronização (isso) ou não vem naturalmente .
fonte
Um bloqueio é usado para visibilidade ou para proteger alguns dados contra modificações simultâneas que podem levar à corrida.
Quando você precisa apenas fazer operações do tipo primitivo para ser atômico, existem opções disponíveis, como
AtomicInteger
e gostos.Mas suponha que você tenha dois números inteiros relacionados entre si, como
x
ey
coordenadas, que são relacionados entre si e devem ser alterados de maneira atômica. Então você os protegeria usando o mesmo bloqueio.Um bloqueio deve proteger apenas o estado que está relacionado um ao outro. Nem menos e nem mais. Se você usar
synchronized(this)
em cada método, mesmo que o estado da classe não seja relacionado, todos os threads enfrentarão contenção, mesmo que atualizem o estado não relacionado.No exemplo acima, eu tenho apenas um método que modifica ambos
x
ey
não dois métodos diferentes, comox
ey
estão relacionados, e se eu tivesse dado dois métodos diferentes para mutaçãox
ey
thread-safe separadamente, então não teria sido.Este exemplo é apenas para demonstrar e não necessariamente o modo como deve ser implementado. A melhor maneira de fazer isso seria torná-lo imutável .
Agora, em oposição ao
Point
exemplo, há um exemplo deTwoCounters
já fornecido por @Andreas em que o estado que está sendo protegido por dois bloqueios diferentes, pois o estado não está relacionado um ao outro.O processo de usar bloqueios diferentes para proteger estados não relacionados é chamado de Strip Striping ou Lock Splitting
fonte
O motivo para não sincronizar isso é que, às vezes, você precisa de mais de um bloqueio (o segundo bloqueio geralmente é removido após algumas reflexões adicionais, mas você ainda precisa no estado intermediário). Se você trava isso , sempre precisa se lembrar qual dos dois bloqueios é esse ; se você bloquear um objeto particular, o nome da variável informa isso.
Do ponto de vista do leitor, se você se deparar com isso , sempre terá que responder às duas perguntas:
Um exemplo:
Se dois encadeamentos começam
longOperation()
em duas instâncias diferentes deBadObject
, eles adquirem seus bloqueios; quando é hora de invocarl.onMyEvent(...)
, temos um impasse porque nenhum dos threads pode adquirir o bloqueio do outro objeto.Neste exemplo, podemos eliminar o impasse usando dois bloqueios, um para operações curtas e outro para operações longas.
fonte
BadObject
A invocalongOperation
B, passando AmyListener
e vice-versa. Não é impossível, mas é bastante complicado, apoiando meus argumentos anteriores.Como já foi dito aqui, o bloco sincronizado pode usar a variável definida pelo usuário como objeto de bloqueio, quando a função sincronizada usa apenas "this". E é claro que você pode manipular com áreas de sua função que devem ser sincronizadas e assim por diante.
Mas todo mundo diz que não há diferença entre a função sincronizada e o bloco, que cobre toda a função usando "this" como objeto de bloqueio. Isso não é verdade, a diferença está no código de bytes que será gerado nas duas situações. No caso de uso sincronizado de blocos, deve ser alocada uma variável local que contém referência a "this". E, como resultado, teremos um tamanho de função um pouco maior (não relevante se você tiver apenas um pequeno número de funções).
Explicação mais detalhada da diferença que você pode encontrar aqui: http://www.artima.com/insidejvm/ed2/threadsynchP.html
Além disso, o uso do bloco sincronizado não é bom devido ao seguinte ponto de vista:
Para obter mais detalhes nesta área, recomendo que você leia este artigo: http://java.dzone.com/articles/synchronized-considered
fonte
Isso é realmente apenas complementar às outras respostas, mas se a sua principal objeção ao uso de objetos particulares para bloqueio é que ela atrapalha sua classe com campos que não estão relacionados à lógica de negócios, o Project Lombok precisa
@Synchronized
gerar o clichê em tempo de compilação:compila para
fonte
Um bom exemplo de uso sincronizado (isso).
Como você pode ver aqui, usamos a sincronização para facilitar a cooperação longa (possivelmente o método de loop infinito de execução) com alguns métodos sincronizados.
Obviamente, pode ser reescrita com muita facilidade usando o sincronizado no campo privado. Mas, às vezes, quando já temos algum design com métodos sincronizados (por exemplo, classe herdada, da qual derivamos, sincronizado (isso) pode ser a única solução).
fonte
this
. Pode ser um campo privado.Depende da tarefa que você deseja executar, mas eu não a usaria. Além disso, verifique se a economia de thread que você deseja acomodar não pôde ser realizada sincronizando (isto) em primeiro lugar? Também existem alguns bloqueios agradáveis na API que podem ajudá-lo :)
fonte
Eu só quero mencionar uma solução possível para referências particulares exclusivas em partes atômicas do código sem dependências. Você pode usar um Hashmap estático com bloqueios e um método estático simples chamado atomic () que cria referências necessárias automaticamente usando informações da pilha (nome completo da classe e número da linha). Em seguida, você pode usar esse método nas instruções de sincronização sem escrever um novo objeto de bloqueio.
fonte
Evite usar
synchronized(this)
como um mecanismo de bloqueio: Isso bloqueia toda a instância da classe e pode causar conflitos. Nesses casos, refatorar o código para bloquear apenas um método ou variável específica, para que toda a classe não seja bloqueada.Synchronised
pode ser usado dentro do nível do método.Em vez de usar
synchronized(this)
, o código abaixo mostra como você pode bloquear um método.fonte
Meus dois centavos em 2019, embora essa questão já pudesse ter sido resolvida.
Bloquear 'this' não é ruim se você souber o que está fazendo, mas nos bastidores bloqueá-lo '(que infelizmente é permitido pela palavra-chave sincronizada na definição do método).
Se você realmente deseja que os usuários de sua classe possam 'roubar' seu bloqueio (ou seja, impedir que outros threads lidem com ele), você realmente deseja que todos os métodos sincronizados esperem enquanto outro método de sincronização estiver em execução e assim por diante. Deve ser intencional e bem pensado (e, portanto, documentado para ajudar seus usuários a entendê-lo).
Para elaborar ainda mais, ao contrário, você deve saber o que está 'ganhando' (ou 'perdendo') se trancar uma trava não acessível (ninguém pode 'roubá-la'), você está no controle total e assim por diante. ..)
O problema para mim é que a palavra-chave sincronizada na assinatura de definição de método facilita demais para os programadores não pensem no que travar, o que é uma coisa muito importante para se pensar, se você não quer encontrar problemas de várias maneiras. programa rosqueado.
Não se pode argumentar que 'tipicamente' você não deseja que os usuários da sua classe possam fazer essas coisas ou que 'tipicamente' você deseja ... Depende de qual funcionalidade você está codificando. Você não pode fazer uma regra de ouro, pois não pode prever todos os casos de uso.
Considere, por exemplo, o gravador de impressão que usa uma trava interna, mas as pessoas lutam para usá-la a partir de vários threads, se não quiserem que a saída seja intercalada.
Se o seu bloqueio estiver acessível fora da classe ou não, a decisão é de você como programador, com base nas funcionalidades da classe. Faz parte da API. Você não pode se afastar, por exemplo, de sincronizado (isto) para sincronizado (provateObjet) sem correr o risco de quebrar as alterações no código que o utiliza.
Nota 1: Eu sei que você pode conseguir o que quer que seja sincronizado (isso) 'consegue' usando um objeto de bloqueio explícito e expô-lo, mas acho desnecessário se o seu comportamento estiver bem documentado e você realmente souber o que significa 'bloquear'.
Nota 2: Não concordo com o argumento de que, se algum código está roubando acidentalmente seu bloqueio, é um bug e você deve resolvê-lo. De certa forma, é o mesmo argumento de dizer que posso tornar todos os meus métodos públicos, mesmo que eles não sejam públicos. Se alguém está chamando 'acidentalmente' meu método destinado a ser privado, é um bug. Por que ativar esse acidente em primeiro lugar !!! Se a habilidade de roubar sua fechadura é um problema para sua classe, não permita. Tão simples como isso.
fonte
Eu acho que os pontos um (outra pessoa usando seu bloqueio) e dois (todos os métodos usando o mesmo bloqueio desnecessariamente) podem acontecer em qualquer aplicativo bastante grande. Especialmente quando não há uma boa comunicação entre os desenvolvedores.
Não é feito de pedra, é principalmente uma questão de boas práticas e prevenção de erros.
fonte