W3docs

Métodos e Blocos Sincronizados em Java

Use métodos e blocos synchronized em Java para proteger seções críticas e escolha o objeto de lock correto.

O capítulo anterior estabeleceu o que synchronized faz. Este é o capítulo sintático — as três formas que a palavra-chave pode assumir, qual lock cada forma usa e como escolher a mais adequada. A forma escolhida tem consequências de desempenho e correção; "simplesmente colocar synchronized no método" funciona em casos triviais e falha quando a classe cresce.

Três formas, três objetos de lock

FormaObjeto de lockQuando usar
synchronized void method()thisClasses pequenas e simples. Lock público está OK.
synchronized static void method()ClassName.classMutação de estado por classe a partir de qualquer instância.
synchronized (obj) { ... }objQuase tudo o mais. Use um lock privado por segurança.

A terceira forma é a mais flexível. As duas primeiras são açúcar sintático para ela.

synchronized em um método de instância

public synchronized void deposit(int x) {
  balance += x;
}

Compila para um bloco que faz lock em this. Apenas uma thread por vez pode estar executando qualquer método de instância sincronizado nesse objeto específico. (Instâncias diferentes de Account têm referências this diferentes e, portanto, monitores diferentes.) Métodos estáticos e métodos não sincronizados não são afetados.

A armadilha. this faz parte da referência pública. Qualquer código que tenha uma referência ao objeto pode fazer synchronized (account) { ... } e manter o mesmo lock que account.deposit(). Isso inclui frameworks de teste, depuradores, código de framework e qualquer outro local de chamada que você não controla. Um chamador mal-intencionado pode manter seu lock pelo tempo que quiser e você ficará bloqueado.

Em classes pequenas, você será o único chamador — tudo bem. Em bibliotecas, em código que outras pessoas vão usar, ou em classes que você pode refatorar mais tarde, prefira um objeto de lock privado.

synchronized em um método estático

public class Counters {
  private static int total;

  public static synchronized void bump() {
    total++;
  }
}

Compila para um bloco que faz lock em Counters.class. O monitor é global por classe — todas as threads, todas as instâncias, disputam o mesmo lock ao chamar bump(). A mesma ressalva de this se aplica: qualquer outro código também pode fazer synchronized (Counters.class) { ... } e manter o lock.

Para estado por classe, essa forma funciona bem em pequenas classes utilitárias. Para classes maiores, prefira um lock estático privado:

public class Counters {
  private static final Object LOCK = new Object();
  private static int total;

  public static void bump() {
    synchronized (LOCK) { total++; }
  }
}

synchronized em um objeto explícito — a forma de produção

public class Cache {
  private final Object lock = new Object();
  private final Map<String, String> data = new HashMap<>();

  public String get(String k) {
    synchronized (lock) {
      return data.get(k);
    }
  }

  public void put(String k, String v) {
    synchronized (lock) {
      data.put(k, v);
    }
  }
}

Duas propriedades que essa forma oferece:

  • Lock privado. Nenhum chamador pode adquiri-lo; ninguém pode bloqueá-lo.
  • Escopo cirúrgico. Apenas o interior do bloco mantém o lock. Tudo fora — validação de argumentos, formatação do valor de retorno, logging — é executado sem contenção.

Pelo mesmo motivo que você mantém campos private final privados, você mantém seu lock privado. O objeto de lock faz parte da sua implementação, não da sua interface.

Regra: mantenha a seção crítica compacta

Quanto mais código executar enquanto um lock é mantido, mais contenção você cria. O padrão correto é fazer o mínimo necessário dentro do bloco:

// Bad: I/O inside the lock — everybody waits while one thread talks to disk
public synchronized void load(String k) {
  String v = Files.readString(Path.of("/tmp/" + k));         // bad
  cache.put(k, v);
}

// Good: read outside the lock, lock only the mutation
public void load(String k) throws IOException {
  String v = Files.readString(Path.of("/tmp/" + k));
  synchronized (lock) {
    cache.put(k, v);
  }
}

O princípio geral: faça lock apenas ao modificar estado compartilhado, nunca ao realizar trabalho arbitrário que pode bloquear.

Ações compostas e duplo lock

synchronized protege um bloco. Se duas operações juntas precisam ser atômicas, ambas devem estar no mesmo bloco:

// Wrong: the if and the put are individually synchronised by HashMap... no they're not,
// but even if they were, the gap between them is not.
if (!map.containsKey(k)) {                            // someone else could insert here
  map.put(k, v);
}

// Right: one block protects both ops
synchronized (lock) {
  if (!map.containsKey(k)) {
    map.put(k, v);
  }
}

// Even better: a single atomic operation
map.putIfAbsent(k, v);                                // for ConcurrentHashMap, fully atomic

A corrida entre containsKey e put — conhecida como a corrida verificar-então-agir — é a fonte de mais bugs de concorrência do que o próprio lock. Sempre que você escrever if (...) doThing(), pergunte: entre o if e o doThing, outra thread pode mudar a resposta? Se sim, atomize.

Locks não se compõem — cuidado com a ordem de lock

Dois blocos synchronized adquiridos em ordens diferentes por threads diferentes podem causar deadlock:

// Thread A
synchronized (account1) {
  synchronized (account2) { transfer(account1, account2, 100); }
}

// Thread B simultaneously
synchronized (account2) {
  synchronized (account1) { transfer(account2, account1, 100); }
}

Cada thread mantém um lock e espera pelo outro. Ambas as threads ficam BLOCKED para sempre. A solução é uma ordem consistente — sempre adquirir locks na mesma ordem global:

void transfer(Account a, Account b, int x) {
  Account first  = a.id() < b.id() ? a : b;          // ordering by stable key
  Account second = a.id() < b.id() ? b : a;
  synchronized (first) {
    synchronized (second) {
      a.debit(x);
      b.credit(x);
    }
  }
}

Ordenação baseada em hash, ordenação baseada em System.identityHashCode ou um lock de desempate são as três abordagens habituais. O capítulo sobre deadlock as cobre em profundidade.

E quanto ao synchronized em um primitivo?

Não é possível. synchronized requer um objeto — um long ou int não tem monitor. Você pode encapsulá-lo (Long/Integer) e sintaticamente fazer lock nele, mas nunca faça isso: primitivos encapsulados no cache de autoboxing são compartilhados. Dois trechos de código que fazem lock em Integer.valueOf(1) estão fazendo lock no mesmo objeto — mesmo que não tenham nada a ver um com o outro.

synchronized (Integer.valueOf(1)) {                   // never do this
  ...
}

Para objetos de lock, sempre aloque um Object privado. O propósito de um monitor é a identidade, não o valor.

synchronized e exceções

Se o corpo de um bloco sincronizado lançar uma exceção, o monitor é liberado conforme a exceção se propaga. Você não precisa de um finally para o desbloqueio — a JVM cuida disso. Essa é uma das principais razões pelas quais synchronized é difícil de usar incorretamente: não há "vazamento de lock" como ocorre com a API explícita Lock cujos métodos lock()/unlock() (onde o desbloqueio é uma chamada de método separada que você deve lembrar de colocar em um finally).

O lado oposto: qualquer estado compartilhado que você tenha modificado pela metade dentro do bloco é visível para o próximo adquirente. Se a exceção deixar seus invariantes quebrados, o lock sozinho não vai te salvar — restaure os invariantes no catch ou projete a mutação para que não possa ser parcialmente concluída.

Um exemplo completo: as quatro formas lado a lado

O programa abaixo usa cada forma contra o mesmo estado compartilhado e termina com uma comparação lado a lado.

java— editable, runs on the server

O que observar na execução:

  • Todas as três formas de contador produziram a contagem esperada 800.000. Cada forma escolheu um objeto de lock diferente (this, um Object privado, a Class), mas cada uma protegeu a operação de leitura-modificação-escrita da mesma forma. synchronized não se importa com o que o objeto de lock é — apenas que todas as threads concorrentes usem o mesmo.
  • A forma de método estático (V3) usou o monitor V3.class como lock. Toda thread, todo teste, todo outro trecho de código que sincronizar em V3.class disputaria o mesmo lock. Isso é adequado para estado por classe; usá-lo para estado por instância é um bug de contenção — você estaria fazendo lock de trabalho não relacionado contra si mesmo.
  • As formas de método estático e de instância são convenientes, mas fazem lock em um objeto publicamente acessível (this ou a Class). Qualquer código pode fazer synchronized (someObject) e manter o mesmo monitor. A forma de objeto de lock privado (V2) é o que o código de produção usa precisamente porque ninguém fora da classe pode alcançar o lock.
  • A classe V4 (definida mas não avaliada acima) mostra a forma errada: trabalho semelhante a I/O dentro da seção crítica. A versão correta seguinte tira a formatação e a chamada (simulada) de bloqueio fora do bloco synchronized, de modo que a contenção seja apenas sobre o put real. A mesma correção, com muito maior throughput sob carga.
  • O bloco de duplo lock no final adquiriu dois locks não relacionados na ordem determinada por System.identityHashCode. Essa regra de ordenação, aplicada em todo o programa, é a estratégia mais simples de prevenção de deadlock quando você precisa manter dois locks ao mesmo tempo. Veremos isso novamente no capítulo sobre deadlock.

O que vem a seguir

O próximo capítulo, Comunicação entre Threads em Java, apresenta a outra metade da API de monitor intrínseco — wait, notify e notifyAll — a forma como as threads se sinalizam dentro de uma seção crítica.

Prática

Prática
Você escreve `public synchronized void deposit(int x)` em um método de `class Account`. Qual monitor o método adquire?
Você escreve `public synchronized void deposit(int x)` em um método de `class Account`. Qual monitor o método adquire?
Was this page helpful?