Verrou de méthode synchronisée Java sur un objet ou une méthode?

191

Si j'ai 2 méthodes synchronisées dans la même classe, mais chacune accédant à des variables différentes, est-ce que 2 threads peuvent accéder à ces 2 méthodes en même temps? Le verrou se produit-il sur l'objet ou est-il aussi spécifique que les variables à l'intérieur de la méthode synchronisée?

Exemple:

class X {

    private int a;
    private int b;

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

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

}

Deux threads peuvent-ils accéder à la même instance de classe X exécutant x.addA() et x.addB()en même temps?

wuntee
la source

Réponses:

199

Si vous déclarez la méthode comme synchronisée (comme vous le faites en tapant public synchronized void addA()), vous synchronisez sur l' objet entier , donc deux threads accédant à une variable différente à partir de ce même objet se bloqueraient de toute façon.

Si vous souhaitez synchroniser uniquement sur une variable à la fois, afin que deux threads ne se bloquent pas lors de l'accès à différentes variables, vous devez les synchroniser séparément en synchronized ()blocs. Si aet bétaient des références d'objet, vous utiliseriez:

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

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

Mais comme ce sont des primitifs, vous ne pouvez pas faire cela.

Je vous suggère d'utiliser plutôt AtomicInteger :

import java.util.concurrent.atomic.AtomicInteger;

class X {

    AtomicInteger a;
    AtomicInteger b;

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

    public void addB(){ 
        b.incrementAndGet();
    }
}
OscarRyz
la source
181
Si vous synchronisez sur la méthode, vous verrouillez tout l'objet, donc deux threads accédant à une variable différente à partir de ce même objet se bloqueraient de toute façon. C'est un peu trompeur. La synchronisation sur la méthode équivaut fonctionnellement à avoir un synchronized (this)bloc autour du corps de la méthode. L'objet «this» ne se verrouille pas, mais l'objet «this» est utilisé comme mutex et le corps est empêché de s'exécuter simultanément avec d'autres sections de code également synchronisées sur «this». Cela n'a aucun effet sur les autres champs / méthodes de "this" qui ne sont pas synchronisés.
Mark Peters
13
Oui, c'est vraiment trompeur. Pour un exemple réel - Regardez ceci - stackoverflow.com/questions/14447095/… - Résumé: Le verrouillage est uniquement au niveau de la méthode synchronisée et les variables d'instance de l'objet sont accessibles par un autre thread
mac
5
Le premier exemple est fondamentalement cassé. Si aet bétaient des objets, par exemple Integers, vous synchronisiez sur des instances que vous remplacez par des objets différents lors de l'application de l' ++opérateur.
Holger
corrigez votre réponse et initialisez AtomicInteger: AtomicInteger a = new AtomicInteger (0);
Mehdi
Peut-être que cette réponse devrait être mise à jour avec l'explication dans cette autre sur la synchronisation sur l'objet lui-même: stackoverflow.com/a/10324280/1099452
lucasvc
71

Synchronisé sur la déclaration de méthode, le sucre syntaxique est pour cela:

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

Sur une méthode statique, c'est du sucre syntaxique pour cela:

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

Je pense que si les concepteurs Java savaient alors ce que l'on comprend maintenant sur la synchronisation, ils n'auraient pas ajouté le sucre syntaxique, car cela conduit le plus souvent à de mauvaises implémentations de concurrence.

Yishai
la source
3
Pas vrai. La méthode synchronized génère un bytecode différent de synchronized (object). Bien que la fonctionnalité soit équivalente, c'est plus qu'un simple sucre syntaxique.
Steve Kuo
10
Je ne pense pas que le «sucre syntaxique» soit strictement défini comme un équivalent octet-code. Le fait est qu'il est fonctionnellement équivalent.
Yishai
1
Si les concepteurs Java avaient su ce que l'on savait déjà sur les moniteurs, ils l'auraient / auraient dû le faire différemment, au lieu d'émuler fondamentalement les entrailles d'Unix. Per Brinch Hansen a déclaré "clairement que j'ai travaillé en vain" quand il a vu les primitives de concurrence Java .
Marquis de Lorne
C'est vrai. L'exemple donné par OP semble verrouiller chaque méthode mais en fait elles se verrouillent toutes sur le même objet. Syntaxe très trompeuse. Après avoir utilisé Java pendant plus de 10 ans, je ne le savais pas. J'éviterais donc les méthodes synchronisées pour cette raison. J'ai toujours pensé qu'un objet invisible était créé pour chaque méthode définie avec synchronized.
Peter Quiring
21

À partir de "Les didacticiels Java ™" sur les méthodes synchronisées :

Premièrement, il n'est pas possible que deux invocations de méthodes synchronisées sur le même objet s'entrelacent. Lorsqu'un thread exécute une méthode synchronisée pour un objet, tous les autres threads qui invoquent des méthodes synchronisées pour le même objet bloquent (suspendent l'exécution) jusqu'à ce que le premier thread soit terminé avec l'objet.

Depuis «Les didacticiels Java ™» sur les blocs synchronisés :

Les instructions synchronisées sont également utiles pour améliorer la concurrence grâce à une synchronisation fine. Supposons, par exemple, que la classe MsLunch ait deux champs d'instance, c1 et c2, qui ne sont jamais utilisés ensemble. Toutes les mises à jour de ces champs doivent être synchronisées, mais il n'y a aucune raison d'empêcher une mise à jour de c1 d'être entrelacée avec une mise à jour de c2 - et cela réduit la concurrence en créant un blocage inutile. Au lieu d'utiliser des méthodes synchronisées ou d'utiliser autrement le verrou associé, nous créons deux objets uniquement pour fournir des verrous.

(Je souligne le mien)

Supposons que vous ayez 2 variables non entrelacées . Vous voulez donc accéder à chacun à partir d'un thread différent en même temps. Vous devez définir le verrou non pas sur la classe d'objet elle-même, mais sur la classe Object comme ci-dessous (exemple du deuxième lien 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
la source
14

Le verrou auquel on accède est sur l'objet, pas sur la méthode. Les variables auxquelles on accède dans la méthode ne sont pas pertinentes.

L'ajout de «synchronisé» à la méthode signifie que le thread exécutant le code doit acquérir le verrou sur l'objet avant de continuer. L'ajout de "statique synchronisé" signifie que le thread exécutant le code doit acquérir le verrou sur l'objet de classe avant de continuer. Vous pouvez également envelopper le code dans un bloc comme celui-ci:

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

afin que vous puissiez spécifier l'objet dont le verrou doit être acquis.

Si vous souhaitez éviter de verrouiller l'objet contenant, vous pouvez choisir entre:

Nathan Hughes
la source
7

À partir de la documentation d'Oracle lien de

La synchronisation des méthodes a deux effets:

Premièrement, il n'est pas possible que deux invocations de méthodes synchronisées sur le même objet s'entrelacent. Lorsqu'un thread exécute une méthode synchronisée pour un objet, tous les autres threads qui invoquent des méthodes synchronisées pour le même objet bloquent (suspendent l'exécution) jusqu'à ce que le premier thread soit terminé avec l'objet.

Deuxièmement, lorsqu'une méthode synchronisée se termine, elle établit automatiquement une relation d'avance avec toute invocation ultérieure d'une méthode synchronisée pour le même objet. Cela garantit que les modifications de l'état de l'objet sont visibles par tous les threads

Consultez cette page de documentation pour comprendre les verrous intrinsèques et le comportement des verrous.

Cela répondra à votre question: sur le même objet x, vous ne pouvez pas appeler x.addA () et x.addB () en même temps lorsqu'une des méthodes synchronisées est en cours d'exécution.

Aditya W
la source
4

Si vous avez des méthodes qui ne sont pas synchronisées et qui accèdent et modifient les variables d'instance. Dans votre exemple:

 private int a;
 private int b;

n'importe quel nombre de threads peut accéder à ces méthodes non synchronisées en même temps lorsqu'un autre thread est dans la méthode synchronisée du même objet et peut apporter des modifications aux variables d'instance. Par exemple: -

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

Vous devez éviter le scénario selon lequel des méthodes non synchronisées accèdent aux variables d'instance et les modifient, sinon il est inutile d'utiliser des méthodes synchronisées.

Dans le scénario ci-dessous: -

class X {

        private int a;
        private int b;

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

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

Un seul des threads peut être dans la méthode addA ou addB, mais en même temps, n'importe quel nombre de threads peut entrer la méthode changeState. Deux threads ne peuvent pas entrer addA et addB en même temps (en raison du verrouillage au niveau de l'objet) mais en même temps n'importe quel nombre de threads peuvent entrer changeState.

Goyal Vicky
la source
3

Vous pouvez faire quelque chose comme ce qui suit. Dans ce cas, vous utilisez le verrou sur a et b pour synchroniser au lieu du verrou sur «ceci». Nous ne pouvons pas utiliser int car les valeurs primitives n'ont pas de verrous, nous utilisons donc Integer.

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

Oui, cela bloquera l'autre méthode car la méthode synchronisée s'applique à l' objet de classe TOUT comme pointé .... mais de toute façon, elle bloquera l'exécution de l'autre thread UNIQUEMENT lors de l'exécution de la somme dans n'importe quelle méthode addA ou addB qu'elle entre, car quand elle se termine ... l'un des threads libérera l'objet et l'autre thread accédera à l'autre méthode et ainsi de suite fonctionnera parfaitement.

Je veux dire que le "synchronisé" est fait précisément pour empêcher l'autre thread d'accéder à un autre pendant l'exécution d'un code spécifique. Donc, finalement, ce code fonctionnera très bien.

En guise de note finale, s'il y a des variables 'a' et 'b', pas seulement une variable unique 'a' ou tout autre nom, il n'est pas nécessaire de synchroniser ces méthodes car il est parfaitement sûr d'accéder à d'autres var (Other memory emplacement).

class X {

private int a;
private int b;

public void addA(){
    a++;
}

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

Fonctionnera aussi

José Velandia
la source
2

Cet exemple (bien que pas joli) peut fournir plus d'informations sur le mécanisme de verrouillage. Si incrementA est synchronisé , et incrementB n'est pas synchronisé , alors incrementB sera exécuté dès que possible, mais si incrementB est également synchronisé, alors il doit `` attendre '' que incrementA se termine, avant qu'incrementB puisse faire son travail.

Les deux méthodes sont appelées sur une seule instance - objet, dans cet exemple, il s'agit de: job , et les threads «concurrents» sont un thread et main .

Essayez avec ' synchronized ' dans incrementB et sans cela et vous verrez des résultats différents.Si incrementB est également ' synchronized ', alors il doit attendre que incrementA () se termine. Exécutez plusieurs fois chaque 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
la source
1

Dans la synchronisation java, si un thread veut entrer dans la méthode de synchronisation, il acquiert le verrouillage sur toutes les méthodes synchronisées de cet objet et pas seulement sur une méthode synchronisée que le thread utilise. Ainsi, un thread exécutant addA () acquiert un verrou sur addA () et addB () car les deux sont synchronisés, donc les autres threads avec le même objet ne peuvent pas exécuter addB ().

Sreedhar Reddy
la source
0

Cela peut ne pas fonctionner car le boxing et l'autoboxing de Integer vers int et vice versa dépendent de JVM et il est fort possible que deux nombres différents soient hachés à la même adresse s'ils sont compris entre -128 et 127.

Sriharsha grv
la source