Bloqueio de método sincronizado Java no objeto ou método?

191

Se eu tiver 2 métodos sincronizados na mesma classe, mas cada um acessar variáveis ​​diferentes, 2 threads poderão acessar esses 2 métodos ao mesmo tempo? O bloqueio ocorre no objeto ou é tão específico quanto as variáveis ​​dentro do método sincronizado?

Exemplo:

class X {

    private int a;
    private int b;

    public synchronized void addA(){
        a++;
    }

    public synchronized void addB(){
        b++;
    }

}

2 threads podem acessar a mesma instância da classe X em execução x.addA() e x.addB()ao mesmo tempo?

voluntário
fonte

Respostas:

197

Se você declarar o método como sincronizado (como você está digitando public synchronized void addA()), sincronizará em todo o objeto, portanto, dois threads acessando uma variável diferente desse mesmo objeto se bloqueariam de qualquer maneira.

Se você deseja sincronizar apenas uma variável de cada vez, para que dois encadeamentos não se bloqueiem ao acessar variáveis ​​diferentes, sincronize-os separadamente em synchronized ()blocos. Se ae bfossem referências a objetos, você usaria:

public void addA() {
    synchronized( a ) {
        a++;
    }
}

public void addB() {
    synchronized( b ) {
        b++;
    }
}

Mas como são primitivas, você não pode fazer isso.

Eu sugiro que você use o AtomicInteger :

import java.util.concurrent.atomic.AtomicInteger;

class X {

    AtomicInteger a;
    AtomicInteger b;

    public void addA(){
        a.incrementAndGet();
    }

    public void addB(){ 
        b.incrementAndGet();
    }
}
OscarRyz
fonte
181
Se você sincronizar com o método, bloqueia o objeto inteiro; portanto, dois threads acessando uma variável diferente desse mesmo objeto se bloqueariam de qualquer maneira. Isso é um pouco enganador. A sincronização no método é funcionalmente equivalente a ter um synchronized (this)bloco ao redor do corpo do método. O objeto "this" não fica bloqueado, mas o objeto "this" é usado como o mutex e o corpo é impedido de executar simultaneamente com outras seções de código também sincronizadas em "this". Não tem efeito em outros campos / métodos "this" que não são sincronizados.
Mark Peters
13
Sim, é realmente enganoso. Para um exemplo real - veja isso - stackoverflow.com/questions/14447095/… - Resumo: O bloqueio é apenas no nível do método sincronizado e as variáveis ​​de instância do objeto podem ser acessadas por outro thread
mac
5
O primeiro exemplo é fundamentalmente quebrado. Se ae bforam objetos, por exemplo, Integers, você estava sincronizando em instâncias que você está substituindo por objetos diferentes ao aplicar o ++operador.
Holger
corrija sua resposta e inicialize o AtomicInteger: AtomicInteger a = new AtomicInteger (0);
Mehdi
Talvez isso anwser deve ser atualizado com o explicado neste outro sobre a sincronização do objeto em si: stackoverflow.com/a/10324280/1099452
lucasvc
71

Sincronizado na declaração do método, há um açúcar sintático para isso:

 public void addA() {
     synchronized (this) {
          a++;
     }
  }

Em um método estático, é um açúcar sintático para isso:

 ClassA {
     public static void addA() {
          synchronized(ClassA.class) {
              a++;
          }
 }

Eu acho que se os designers de Java soubessem o que é entendido agora sobre sincronização, eles não teriam adicionado o açúcar sintático, pois, na maioria das vezes, leva a implementações ruins de simultaneidade.

Yishai
fonte
3
Não é verdade. O método sincronizado gera bytecode diferente do sincronizado (objeto). Embora a funcionalidade seja equivalente, é mais do que apenas açúcar sintático.
Steve Kuo
10
Eu não acho que "açúcar sintático" seja estritamente definido como equivalente a código de bytes. O ponto é que é funcionalmente equivalente.
Yishai
1
Se os designers de Java soubessem o que era conhecido sobre os monitores, eles teriam / deveriam ter feito de maneira diferente, em vez de basicamente emular as entranhas do Unix. Por Brinch, Hansen disse que 'claramente trabalhei em vão' quando viu as primitivas de simultaneidade Java .
Marquês de Lorne
Isso é verdade. O exemplo dado pelo OP parece bloquear cada método, mas na verdade todos eles bloqueiam no mesmo objeto. Sintaxe muito enganosa. Depois de usar Java por mais de 10 anos, eu não sabia disso. Portanto, eu evitaria métodos sincronizados por esse motivo. Eu sempre pensei que um objeto invisível foi criado para cada método definido com sincronizado.
Peter Quiring
21

Em "Os tutoriais Java ™" sobre métodos sincronizados :

Primeiro, não é possível intercalar duas invocações de métodos sincronizados no mesmo objeto . Quando um encadeamento está executando um método sincronizado para um objeto, todos os outros encadeamentos que invocam métodos sincronizados para o mesmo bloco de objetos (suspender a execução) até que o primeiro encadeamento seja concluído com o objeto.

Em "Os tutoriais Java ™" em blocos sincronizados :

Instruções sincronizadas também são úteis para melhorar a simultaneidade com sincronização refinada. Suponha, por exemplo, que a classe MsLunch tenha dois campos de instância, c1 e c2, que nunca são usados ​​juntos. Todas as atualizações desses campos devem ser sincronizadas, mas não há motivo para impedir que uma atualização de c1 seja intercalada com uma atualização de c2 - e isso reduz a simultaneidade ao criar bloqueios desnecessários. Em vez de usar métodos sincronizados ou usar o bloqueio associado a isso, criamos dois objetos apenas para fornecer bloqueios.

(Ênfase minha)

Suponha que você tenha 2 variáveis não intercaladas . Então, você deseja acessar cada um de diferentes threads ao mesmo tempo. Você precisa definir o bloqueio não na classe de objetos em si, mas na classe Object, como abaixo (exemplo no segundo link do Oracle):

public class MsLunch {

    private long c1 = 0;
    private long c2 = 0;

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

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}
MehdiMAH
fonte
14

O bloqueio acessado está no objeto, não no método. Quais variáveis ​​são acessadas dentro do método são irrelevantes.

Adicionar "sincronizado" ao método significa que o encadeamento que executa o código deve adquirir o bloqueio no objeto antes de continuar. Adicionar "estático sincronizado" significa que o thread que está executando o código deve adquirir o bloqueio no objeto de classe antes de continuar. Como alternativa, você pode agrupar o código em um bloco como este:

public void addA() {
    synchronized(this) {
        a++;
    }
}

para que você possa especificar o objeto cujo bloqueio deve ser adquirido.

Se você deseja evitar o bloqueio do objeto que contém, você pode escolher entre:

Nathan Hughes
fonte
7

No link da documentação da oracle

A sincronização de métodos tem dois efeitos:

Primeiro, não é possível intercalar duas invocações de métodos sincronizados no mesmo objeto. Quando um encadeamento está executando um método sincronizado para um objeto, todos os outros encadeamentos que invocam métodos sincronizados para o mesmo bloco de objetos (suspender a execução) até que o primeiro encadeamento seja concluído com o objeto.

Segundo, quando um método sincronizado sai, ele estabelece automaticamente um relacionamento de antes do acontecimento com qualquer chamada subsequente de um método sincronizado para o mesmo objeto. Isso garante que as alterações no estado do objeto sejam visíveis para todos os threads

Consulte esta página de documentação para entender os bloqueios intrínsecos e o comportamento dos bloqueios.

Isso responderá à sua pergunta: No mesmo objeto x, você não pode chamar x.addA () e x.addB () ao mesmo tempo quando um dos métodos sincronizados estiver em execução.

Aditya W
fonte
4

Se você tiver alguns métodos que não estão sincronizados e estão acessando e alterando as variáveis ​​da instância. No seu exemplo:

 private int a;
 private int b;

qualquer número de threads pode acessar esses métodos não sincronizados ao mesmo tempo em que outro thread estiver no método sincronizado do mesmo objeto e pode fazer alterações nas variáveis ​​da instância. Por exemplo: -

 public void changeState() {
      a++;
      b++;
    }

Você precisa evitar o cenário em que métodos não sincronizados estão acessando as variáveis ​​da instância e alterando-os, caso contrário não há sentido em usar métodos sincronizados.

No cenário abaixo: -

class X {

        private int a;
        private int b;

        public synchronized void addA(){
            a++;
        }

        public synchronized void addB(){
            b++;
        }
     public void changeState() {
          a++;
          b++;
        }
    }

Somente um dos encadeamentos pode estar no método addA ou addB, mas ao mesmo tempo qualquer número de encadeamentos pode inserir o método changeState. Dois threads não podem inserir addA e addB ao mesmo tempo (devido ao bloqueio no nível do objeto), mas ao mesmo tempo qualquer número de threads pode inserir changeState.

Goyal Vicky
fonte
3

Você pode fazer algo como o seguinte. Nesse caso, você está usando o bloqueio em aeb para sincronizar em vez do bloqueio em "this". Não podemos usar int porque os valores primitivos não têm bloqueios; portanto, usamos Inteiro.

class x{
   private Integer a;
   private Integer b;
   public void addA(){
      synchronized(a) {
         a++;
      }
   }
   public synchronized void addB(){
      synchronized(b) {
         b++;
      }
   }
}
dsmith
fonte
3

Sim, ele bloqueará o outro método porque o método sincronizado se aplica ao objeto de classe WHOLE conforme apontado .... mas de qualquer maneira ele bloqueará a execução do outro thread SOMENTE enquanto executa a soma em qualquer método que addA ou addB entre, porque quando terminar ... o um thread libera o objeto e o outro thread acessa o outro método e assim por diante funcionando perfeitamente.

Quero dizer que o "sincronizado" é feito precisamente para impedir que o outro thread acesse outro enquanto estiver em uma execução de código específica. Então, finalmente, esse código funcionará bem.

Como observação final, se houver variáveis ​​'a' e 'b', não apenas uma variável única 'a' ou qualquer outro nome, não será necessário sincronizar esses métodos, pois é perfeitamente seguro acessar outra var (outra memória localização).

class X {

private int a;
private int b;

public void addA(){
    a++;
}

public void addB(){
    b++;
}}

Vai funcionar bem

Jose Velandia
fonte
2

Este exemplo (embora não seja bonito) pode fornecer mais informações sobre o mecanismo de bloqueio. Se incrementoA é sincronizado e incrementoB não é sincronizado , o incrementoB será executado o mais rápido possível, mas se o incrementoB também for sincronizado , será necessário 'aguardar' que o incrementoA termine, antes que o incrementoB possa fazer seu trabalho.

Ambos os métodos são chamados no objeto de instância única, neste exemplo é: job e os threads 'concorrentes' são aThread e main .

Tente com ' sincronizado ' no incrementoB e sem ele, e você verá resultados diferentes. Se o incrementoB também estiver ' sincronizado ', será necessário aguardar a conclusão do incrementoA (). Execute várias vezes cada variante.

class LockTest implements Runnable {
    int a = 0;
    int b = 0;

    public synchronized void incrementA() {
        for (int i = 0; i < 100; i++) {
            this.a++;
            System.out.println("Thread: " + Thread.currentThread().getName() + "; a: " + this.a);
        }
    }

    // Try with 'synchronized' and without it and you will see different results
    // if incrementB is 'synchronized' as well then it has to wait for incrementA() to finish

    // public void incrementB() {
    public synchronized void incrementB() {
        this.b++;
        System.out.println("*************** incrementB ********************");
        System.out.println("Thread: " + Thread.currentThread().getName() + "; b: " + this.b);
        System.out.println("*************** incrementB ********************");
    }

    @Override
    public void run() {
        incrementA();
        System.out.println("************ incrementA completed *************");
    }
}

class LockTestMain {
    public static void main(String[] args) throws InterruptedException {
        LockTest job = new LockTest();
        Thread aThread = new Thread(job);
        aThread.setName("aThread");
        aThread.start();
        Thread.sleep(1);
        System.out.println("*************** 'main' calling metod: incrementB **********************");
        job.incrementB();
    }
}
Nenad Bulatovic
fonte
1

Na sincronização java, se um encadeamento desejar entrar no método de sincronização, ele obterá bloqueio em todos os métodos sincronizados desse objeto, não apenas em um método sincronizado que o encadeamento esteja usando. Portanto, um thread que executa addA () adquirirá bloqueio em addA () e addB (), pois ambos são sincronizados. Portanto, outros threads com o mesmo objeto não podem executar addB ().

Sreedhar Reddy
fonte
0

Isso pode não funcionar, pois o boxe e a autoboxing de Inteiro para int e vice-versa dependem da JVM e existe uma grande possibilidade de que dois números diferentes possam obter hash no mesmo endereço se estiverem entre -128 e 127.

Sriharsha grv
fonte