Evitar sincronizado (isso) em Java?

381

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:

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 ( synchronizedmétodos) e forma explícita de synchronized(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ão synchronized(this)não será aplicável. Portanto, esse não é o problema
eljenso
fonte
4
Eu também gostaria de salientar que o contexto é importante - o bit "Normalmente é o bloqueio na própria instância" está em uma seção sobre a documentação de uma classe condicionalmente segura para threads, quando você está tornando o bloqueio público. Em outras palavras, essa frase se aplica quando você já tomou essa decisão.
11139 Jon Skeet
Na ausência de sincronização interna, e quando a sincronização externa é necessária, o bloqueio geralmente é a própria instância, diz Bloch. Então, por que esse não seria o caso da sincronização interna com o bloqueio de 'this' também? (A importância da documentação é outra questão.)
eljenso
Há uma troca entre granularidade estendida e sobrecarga extra de solicitações de cache e barramento da CPU, porque o bloqueio de um objeto externo provavelmente exigirá que uma linha de cache separada seja modificada e trocada entre os caches da CPU (cf. MESIF e MOESI).
ArtemGr
11
Eu acho que, no mundo da programação defensiva, você evita bugs não pelo idioma, mas pelo código. Quando alguém me pergunta: "Qual é a otimização da sua sincronização?", Quero dizer "Muito" em vez de "Muito, a menos que alguém não siga o idioma".
precisa saber é o seguinte

Respostas:

132

Vou cobrir cada ponto separadamente.

  1. Algum código maligno pode roubar seu bloqueio (este muito popular, também possui uma variante "acidentalmente")

    Estou mais preocupado acidentalmente . O que isso significa é que esse uso thisfaz 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 como Collections.synchronizedMap(veja o javadoc).

  2. Todos os métodos sincronizados da mesma classe usam exatamente o mesmo bloqueio, o que reduz a taxa de transferência

    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.

  3. Você está (desnecessariamente) expondo muita informaçã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.

Darron
fonte
1. "sincronizado" não faz parte da interface exposta da sua classe. 2. concordo 3. veja 1.
eljenso
66
Essencialmente sincronizado (isso) é exposto porque significa que o código externo pode afetar a operação da sua classe. Portanto, afirmo que você deve documentá-lo como interface, mesmo que o idioma não o faça.
Darron
15
Semelhante. Consulte o Javadoc para Collections.synchronizedMap () - o objeto retornado usa sincronizado (this) internamente e espera que o consumidor aproveite isso para usar o mesmo bloqueio para operações atômicas em grande escala, como iteração.
Darron
3
De fato, Collections.synchronizedMap () NÃO usa sincronizado (isso) internamente, ele usa um objeto de bloqueio final privado.
Bas Leijdekkers 22/03
11
@Bas Leijdekkers: a documentação especifica claramente que a sincronização ocorre na instância de mapa retornada. O interessante é que as exibições retornadas keySet()e values()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 subclasse SynchronizedSortedMapprecisa dele para implementar submapas que bloqueiam na instância do mapa original.
Holger
86

Bem, em primeiro lugar, deve-se salientar que:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

é semanticamente equivalente a:

public synchronized void blah() {
  // do stuff
}

qual é uma razão para não usar synchronized(this). Você pode argumentar que pode fazer coisas ao redor do synchronized(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.

cleto
fonte
4
"sincronizado (isso) realmente não oferece nada." Ok, substituo-o por uma sincronização (myPrivateFinalLock). O que isso me dá? Você fala sobre isso ser um mecanismo defensivo. Contra o que estou protegido?
eljenso
14
Você está protegido contra o bloqueio acidental (ou malicioso) de 'this' por objetos externos.
Cletus
14
Não concordo com esta resposta: um bloqueio sempre deve ser mantido pelo menor tempo possível, e é exatamente por isso que você deseja "fazer coisas" em torno de um bloco sincronizado em vez de sincronizar todo o método .
194 Olivier
5
Fazer coisas fora do bloco sincronizado é sempre bem-intencionado. O ponto é que as pessoas entendem isso errado a maior parte do tempo e nem percebem, assim como no problema de bloqueio verificado. O caminho para o inferno é pavimentado com boas intenções.
Cletus
16
Eu discordo em geral de que "X é um mecanismo defensivo, que nunca é uma má idéia". Existe muito código desnecessariamente inchado por causa dessa atitude.
111310 finnw #
54

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:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

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:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

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 ...

Andreas Bakurov
fonte
Isso é muito perigoso. Agora você introduziu um requisito de pedido de bloqueio do lado do cliente (usuário desta classe). Se dois threads estiverem chamando method1 () e method2 () em uma ordem diferente, é provável que eles entrem em conflito, mas o usuário desta classe não tem idéia de que esse é o caso.
daveb 14/01/09
7
A granularidade não fornecida por "sincronizado (isto)" está fora do escopo da minha pergunta. E seus campos de bloqueio não devem ser finais?
eljenso
10
a fim de ter um impasse deve-se realizar uma chamada do bloco sincronizado por A para o bloco sincronizado por B. daveb, você está errado ...
Andreas Bakurov
3
Não há impasse neste exemplo, tanto quanto eu posso ver. Eu aceito é apenas pseudocódigo mas eu gostaria de usar uma das implementações de java.util.concurrent.locks.Lock como java.util.concurrent.locks.ReentrantLock
Shawn Vader
15

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 ServletContextimplementação. Seu getAttributemétodo deve ser seguro para threads, pois os atributos de contexto são dados compartilhados; então você declara como synchronized. 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 initmétodo:

sincronizado (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}

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 getAttributeserá bloqueada para sempre. O hacker conseguiu um DoS no meu servlet.

Esse ataque não é possível se getAttributeestiver 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?

Olivier
fonte
depende do que a classe faz: se for um objeto 'importante', bloqueie a referência privada? Caso contrário, o bloqueio será suficiente?
eljenso
6
Sim, o cenário de roubo de fechadura parece distante para mim. Todo mundo menciona, mas quem realmente fez ou experimentou? Se você "acidentalmente" bloqueia um objeto que não deveria, existe um nome para esse tipo de situação: é um bug. Consertá-lo.
eljenso
4
Além disso, o bloqueio de referências internas não está isento do "ataque de sincronização externa": se você sabe que uma parte do código sincronizada espera que um evento externo aconteça (por exemplo, gravação de arquivo, valor no banco de dados, evento de timer), provavelmente providencie para que ele bloqueie também.
eljenso
Deixe-me confessar que sou um desses idiotas, apesar de ter feito isso quando era jovem. Eu pensei que o código era mais limpo, não criando um objeto de bloqueio explícito e, em vez disso, usei outro objeto final privado que precisava participar do monitor. Eu não sabia que o próprio objeto fazia uma sincronização consigo mesmo. Você pode imaginar o hijinx que se segue ...
Alan Cabrera
12

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.

synchronized(this)
{
  SHARED_ENTITY.....
}

"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.
insira a descrição da imagem aqui

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

insira a descrição da imagem aqui
insira a descrição da imagem aqui

Veja mais sobre Threads ----> aqui

Rohit Singh
fonte
11

Parece haver um consenso diferente nos campos C # e Java sobre isso. A maioria do código Java que eu vi usa:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

considerando que a maioria do código C # opta pelo discutivelmente mais seguro:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

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.

serg10
fonte
3
Talvez como o C # seja uma linguagem mais jovem, eles aprenderam com padrões ruins que foram descobertos no campo Java e codificam coisas assim melhor? Existem também menos singletons? :)
Bill K
3
Ele Ele. Possivelmente, é verdade, mas não vou morder a isca! Alguém que eu possa dizer com certeza é que existem mais letras maiúsculas no código C #;)
serg10 15/01/2009
11
Simplesmente não é verdade (para colocá-lo bem)
tcurdt
7

O java.util.concurrentpacote 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á vi synchronized(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.

jamesh
fonte
6
  1. Torne seus dados imutáveis, se possível ( finalvariáveis)
  2. Se você não pode evitar a mutação de dados compartilhados em vários threads, use construções de programação de alto nível [por exemplo, LockAPI granular ]

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.

Código de amostra para usar ReentrantLockqual implementa a Lockinterface

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Vantagens do bloqueio sobre sincronizado (isso)

  1. 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.

  2. As implementações de bloqueio fornecem funcionalidade adicional sobre o uso de métodos e instruções sincronizados, fornecendo

    1. Uma tentativa sem bloqueio de adquirir um bloqueio ( tryLock())
    2. Uma tentativa de adquirir o bloqueio que pode ser interrompido ( lockInterruptibly())
    3. Uma tentativa de adquirir o bloqueio que pode exceder o tempo limite ( tryLock(long, TimeUnit)).
  3. Uma classe Lock também pode fornecer comportamentos e semânticas bastante diferentes das do bloqueio implícito do monitor, como

    1. pedido garantido
    2. uso não reentrante
    3. Detecção de deadlock

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.

Ravindra babu
fonte
5

Se você decidiu isso:

  • o que você precisa fazer é bloquear o objeto atual; e
  • você deseja bloqueá-lo com granularidade menor que um método inteiro;

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.

Neil Coffey
fonte
11
"se isso fosse esperado pela lógica ..." é um ponto que também estou tentando entender. Não vejo o objetivo de sempre usar bloqueios privados, embora o consenso geral pareça ser de que é melhor, pois não dói e é mais defensivo.
eljenso
4

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

  1. A sincronização no nível do método permite o bloqueio do lado do cliente.
  2. Se você possui um objeto de bloqueio particular, torna impossível o bloqueio do lado do cliente. Isso é bom se você souber que sua classe não possui o tipo de funcionalidade "colocar se ausente".
  3. Se você estiver projetando uma biblioteca - sincronizar com isso ou sincronizar o método geralmente é mais sábio. Porque você raramente está em posição de decidir como sua classe será usada.
  4. Se Vector tivesse usado um objeto de bloqueio privado - teria sido impossível acertar "se estiver ausente". O código do cliente nunca obterá um identificador para o bloqueio privado, quebrando assim a regra fundamental de usar o EXACT SAME LOCK para proteger seu estado.
  5. A sincronização com esse método ou com os métodos sincronizados tem um problema, como outros já apontaram - alguém pode obter um bloqueio e nunca liberá-lo. Todos os outros threads continuariam aguardando a liberação do bloqueio.
  6. Portanto, saiba o que está fazendo e adote o que está correto.
  7. Alguém argumentou que ter um objeto de bloqueio privado oferece uma granularidade melhor - por exemplo, se duas operações não estiverem relacionadas - elas podem ser protegidas por bloqueios diferentes, resultando em melhor rendimento. Mas isso eu acho que é cheiro de design e não cheiro de código - se duas operações são completamente independentes, por que elas fazem parte da mesma classe? Por que um clube de classe não deveria ter funcionalidades relacionadas? Pode ser uma classe de utilidade? Hmmmm - algum utilitário que fornece manipulação de string e formatação de data do calendário na mesma instância? ... não faz nenhum sentido para mim, pelo menos!
Sandesh Sadhale
fonte
3

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ê usa synchronized(labelMonitor), ou melhor ainda labelLock.getWriteLock().lock(), fica claro o que está fazendo e a que se limitam os efeitos da sua seção crítica.

Andrzej Doyle
fonte
3

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 .

tcurdt
fonte
Entendo você corretamente quando diz que depende da sua experiência?
eljenso
Em primeiro lugar, depende do código que você deseja escrever. Apenas estou dizendo que você pode precisar de um pouco mais de experiência ao desviar para não usar a sincronização (isso).
tcurdt 15/01/09
2

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 xey 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.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

No exemplo acima, eu tenho apenas um método que modifica ambos xe ynão dois métodos diferentes, como xe yestão relacionados, e se eu tivesse dado dois métodos diferentes para mutação xey 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 Pointexemplo, há um exemplo de TwoCountersjá 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

Narendra Pathai
fonte
1

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:

  1. que tipo de acesso é protegido por isso ?
  2. é realmente um bloqueio, alguém não introduziu um bug?

Um exemplo:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

Se dois encadeamentos começam longOperation()em duas instâncias diferentes de BadObject, 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.

18446744073709551615
fonte
2
A única maneira de obter um impasse neste exemplo é quando BadObjectA invoca longOperationB, passando A myListenere vice-versa. Não é impossível, mas é bastante complicado, apoiando meus argumentos anteriores.
eljenso
1

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:

A palavra-chave sincronizada é muito limitada em uma área: ao sair de um bloco sincronizado, todos os threads que aguardam esse bloqueio devem ser desbloqueados, mas apenas um desses threads consegue o bloqueio; todos os outros veem que o bloqueio foi efetuado e voltam ao estado bloqueado. Isso não é apenas muitos ciclos de processamento desperdiçados: geralmente a mudança de contexto para desbloquear um encadeamento também envolve a paginação de memória do disco, e isso é muito, muito, caro.

Para obter mais detalhes nesta área, recomendo que você leia este artigo: http://java.dzone.com/articles/synchronized-considered

romano
fonte
1

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 @Synchronizedgerar o clichê em tempo de compilação:

@Synchronized
public int foo() {
    return 0;
}

compila para

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}
Michael
fonte
0

Um bom exemplo de uso sincronizado (isso).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

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).

Bart Prokop
fonte
Qualquer objeto pode ser usado como o bloqueio aqui. Não precisa ser this. Pode ser um campo privado.
finnw
Correto, mas o objetivo deste exemplo era mostrar como fazer a sincronização adequada, se decidirmos usar a sincronização de método.
Bart Prokop
0

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 :)

Harald Schilly
fonte
0

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.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}
Kolarčík Václav
fonte
0

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. Synchronisedpode 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.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }
surendrapanday
fonte
0

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.

Amit Mittal
fonte
-3

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.

Yoni Roit
fonte