Question

Chaque fois qu'une question sur la synchronisation Java sur une SO apparaît, certaines personnes ont très envie de souligner que synchronized (this) doit être évité. Au lieu de cela, ils prétendent, un verrou sur une référence privée doit être préféré.

Certaines des raisons données sont:

D'autres personnes, y compris moi-même, affirment que synchronized (this) est un idiome très utilisé (également dans les bibliothèques Java), qu'il est sûr et bien compris. Cela ne devrait pas être évité car vous avez un bogue et vous n'avez aucune idée de ce qui se passe dans votre programme multithread. En d’autres termes: s’il est applicable, utilisez-le.

Je suis intéressé par des exemples concrets (pas d'objets spécifiques) dans lesquels il est préférable d'éviter de verrouiller this lorsque synchronized (this) fait également l'affaire. .

Par conséquent: devriez-vous toujours éviter de synchroniser (this) et le remplacer par un verrou sur une référence privée?

Quelques informations supplémentaires (mises à jour à mesure que les réponses sont fournies):

  • nous parlons de synchronisation d'instance
  • les deux formes implicites (méthodes synchronisées ) et explicites de synchronisées (this) sont considérées
  • si vous citez Bloch ou d'autres autorités sur le sujet, ne laissez pas de côté les parties que vous n'aimez pas (par exemple, Effective Java, élément sur la sécurité des threads: il s'agit généralement du verrou sur l'instance elle-même, mais il y a des exceptions.)
  • si vous avez besoin d'une granularité dans votre verrouillage autre que synchronisée (this) , alors synchronized (this) n'est pas applicable, ce n'est donc pas le problème
Était-ce utile?

La solution

Je couvrirai chaque point séparément.

  1.   

    Certains codes maléfiques peuvent voler votre verrou (très populaire celui-ci, a aussi un   " accidentellement " variante)

    Je suis plus inquiet au sujet de accidentellement . Cela revient à dire que cette utilisation de this fait partie de l'interface exposée de votre classe et doit être documentée. Parfois, la capacité d'un autre code à utiliser votre verrou est souhaitée. Ceci est vrai pour des choses comme Collections.synchronizedMap (voir le javadoc).

  2.   

    Toutes les méthodes synchronisées d'une même classe utilisent exactement la même chose   verrouiller, ce qui réduit le débit

    C’est une pensée trop simpliste; se débarrasser de synchronized (this) ne résoudra pas le problème. Une synchronisation correcte du débit demandera plus de réflexion.

  3.   

    Vous exposez (inutilement) trop d'informations

    Ceci est une variante de # 1. L'utilisation de synchronized (this) fait partie de votre interface. Si vous ne voulez pas / n'avez pas besoin que cela soit exposé, ne le faites pas.

Autres conseils

Eh bien, tout d’abord, il convient de souligner que:

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

est sémantiquement équivalent à:

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

qui est une des raisons pour ne pas utiliser synchronisé (this) . Vous pouvez faire valoir que vous pouvez faire des choses autour du bloc synchronized (this) . La raison habituelle est d’essayer d’éviter de faire la vérification synchronisée, ce qui conduit à toutes sortes de problèmes d’accès simultané, en particulier le double problème de verrouillage vérifié , ce qui montre bien à quel point il peut être difficile de réaliser une vérification relativement simple threadsafe.

Un verrou privé est un mécanisme de défense qui n’est jamais une mauvaise idée.

De plus, comme vous l'avez dit, les verrous privés peuvent contrôler la granularité. Un ensemble d'opérations sur un objet peut ne pas être associé à un autre, mais synchronisé (this) exclura mutuellement l'accès à toutes.

synchronisé (ceci) ne vous donne vraiment rien.

Lorsque vous utilisez synchronized (this), vous utilisez l’instance de classe comme un verrou. Cela signifie que tant que le verrou est acquis par le fil 1 , le fil 2 doit attendre.

Supposons le code suivant:

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


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

Méthode 1 modifiant la variable a et méthode 2 modifiant la variable b , la modification simultanée de la même variable par deux threads doit être évitée et c’est le cas. MAIS lorsque thread1 modifie a et thread2 modifie b , il peut être exécuté sans condition de concurrence.

Malheureusement, le code ci-dessus ne le permettra pas car nous utilisons la même référence pour un verrou; Cela signifie que les threads, même s'ils ne sont pas en situation de concurrence critique, doivent attendre et que le code sacrifie évidemment la concurrence du programme.

La solution consiste à utiliser 2 différents verrous pour deux variables différentes:

.
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 ++;      
        }
        // ................
    }

}

L'exemple ci-dessus utilise des verrous plus fins (2 verrous au lieu d'un ( lockA et lockB pour les variables a et b respectivement) et permet ainsi une meilleure concurrence, mais est devenu plus complexe que le premier exemple ...

Bien que je convienne de ne pas adhérer aveuglément aux règles dogmatiques, le "vol de cadenas" scénario vous semble si excentrique? Un thread pourrait en effet acquérir le verrou sur votre objet "en externe" ( synchronisé (theObject) {...} ), en bloquant les autres threads en attente de méthodes d'instance synchronisées.

Si vous ne croyez pas en un code malveillant, considérez que ce code peut provenir de tiers (par exemple, si vous développez une sorte de serveur d'applications).

Le " accidentel " la version semble moins probable, mais comme on dit, "rendre quelque chose idiot-preuve et quelqu'un va inventer un meilleur idiot".

Je suis donc d’accord avec l’école de pensée «ça dépend de ce que la classe fait».

Modifiez les 3 premiers commentaires suivants d'eljenso:

Je n'ai jamais rencontré le problème du vol de serrure, mais voici un scénario imaginaire:

Supposons que votre système est un conteneur de servlet et que l'objet que nous considérons est l'implémentation ServletContext . Sa méthode getAttribute doit être adaptée aux threads, car les attributs de contexte sont des données partagées. donc vous le déclarez comme synchronisé . Imaginons également que vous fournissiez un service d'hébergement public basé sur votre implémentation de conteneur.

Je suis votre client et déployez mon "bon". servlet sur votre site. Il se trouve que mon code contient un appel à getAttribute .

Un pirate informatique, déguisé en un autre client, déploie son servlet malveillant sur votre site. Il contient le code suivant dans la méthode init :

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

En supposant que nous partagions le même contexte de servlet (autorisé par la spécification tant que les deux servlets sont sur le même hôte virtuel), mon appel sur getAttribute est verrouillé à jamais. Le pirate informatique a réalisé un déni de service sur mon servlet.

Cette attaque n'est pas possible si getAttribute est synchronisé sur un verrou privé, car le code tiers ne peut pas acquérir ce verrou.

J'admets que l'exemple est artificiel et offre une vue simpliste du fonctionnement d'un conteneur de servlets, mais à mon humble avis, cela prouve le point.

Je choisirais donc mon concept en fonction de considérations de sécurité: aurai-je le contrôle total sur le code ayant accès aux instances? Quelle serait la conséquence d'un thread qui verrouille indéfiniment une instance?

Il semble y avoir un consensus différent dans les camps C # et Java à ce sujet. La majorité du code Java que j'ai vu utilise:

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

alors que la majorité du code C # opte pour le plus sûr sans doute:

// 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
}

Le langage C # est certainement plus sûr. Comme mentionné précédemment, aucun accès malveillant / accidentel au verrou ne peut être effectué en dehors de l'instance. Le code Java comporte également ce risque, , mais il semble que la communauté Java ait évolué au fil du temps vers une version légèrement moins sûre, mais légèrement plus concise.

Cela ne veut pas dire que j'apprécie Java, mais le reflet de mon expérience de travail dans les deux langues.

Cela dépend de la situation.
S'il n'y a qu'une ou plusieurs entités de partage.

Voir l'exemple complet de travail ici

Une petite introduction.

Threads et entités partageables
Il est possible que plusieurs threads accèdent à la même entité, par exemple plusieurs connectionThreads partageant une même file de messages. Étant donné que les threads fonctionnent simultanément, il peut y avoir une chance de remplacer ses données par une autre, ce qui peut être une situation compliquée.
Nous avons donc besoin d'un moyen de nous assurer que cette entité partageable n'est accessible que par un seul thread à la fois. (CONCURRENCE).

Bloc synchronisé
Le bloc synchronized () est un moyen de garantir l'accès simultané d'une entité à partager.
Tout d'abord, une petite analogie
Supposons qu'il y a deux personnes P1, P2 (fils) et un lavabo (entité à partager) dans une salle de toilette et qu'il y a une porte (serrure).
Maintenant, nous voulons qu'une personne utilise le lavabo à la fois.
Une approche consiste à verrouiller la porte en P1 lorsque la porte est verrouillée. P2 attend que p1 ait terminé son travail.
P1 ouvre la porte
alors seul p1 peut utiliser un lavabo.

syntaxe.

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

"this" fourni le verrou intrinsèque associé à la classe (classe Object conçue par le développeur Java de telle sorte que chaque objet puisse fonctionner comme moniteur). L'approche ci-dessus fonctionne bien lorsqu'il n'y a qu'une seule entité partagée et plusieurs threads (1: N).
   entrer la description de l'image ici N entités partageables - M threads
Maintenant, imaginez une situation où il y a deux lavabos dans une salle de toilette et une seule porte. Si nous utilisons l'approche précédente, seul p1 peut utiliser un lavabo à la fois, tandis que p2 attendra à l'extérieur. C'est un gaspillage de ressources car personne n'utilise B2 (lavabo).
Une approche plus sage consisterait à créer une pièce plus petite dans les toilettes et à leur fournir une porte par lavabo. De cette manière, P1 peut accéder à B1 et P2 à B2 et vice-versa.

washbasin1;  
washbasin2;

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

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

 entrez la description de l'image ici
entrer la description de l'image ici

En savoir plus sur les discussions ---- > ici

Le paquetage java.util.concurrent a considérablement réduit la complexité de mon code de sécurité des threads. Je n'ai que des preuves anecdotiques, mais la plupart des travaux que j'ai vus avec synchronized (x) semblent réimplémenter un verrou, un sémaphore ou un verrou, mais en utilisant les moniteurs de niveau inférieur. / p>

Dans cet esprit, la synchronisation à l'aide de l'un de ces mécanismes est analogue à la synchronisation sur un objet interne, plutôt que la perte d'un verrou. Cela présente un avantage en ce sens que vous avez la certitude absolue que vous contrôlez l'entrée dans le moniteur par au moins deux threads.

  1. Rendre vos données immuables si cela est possible (variables final )
  2. Si vous ne pouvez pas éviter la mutation de données partagées sur plusieurs threads, utilisez des constructions de programmation de haut niveau [par exemple. API de verrouillage granulaire
  

Un verrou fournit un accès exclusif à une ressource partagée: un seul thread à la fois peut acquérir le verrou et tout accès à la ressource partagée nécessite que le verrou soit acquis en premier.

Exemple de code à utiliser ReentrantLock qui implémente l'interface Lock

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

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

Avantages du verrouillage sur la synchronisation (this)

  1. L'utilisation de méthodes ou d'instructions synchronisées oblige toutes les acquisitions et les déblocages de verrous à se dérouler sous forme de blocs.

  2. Les implémentations de verrouillage fournissent des fonctionnalités supplémentaires par rapport à l'utilisation de méthodes et d'instructions synchronisées en fournissant

    1. Une tentative non bloquante d'acquérir un verrou ( tryLock () )
    2. Une tentative d'acquisition du verrou pouvant être interrompu ( lockInterruptibly () )
    3. Tentative d'obtention du verrou pouvant atteindre le délai d'expiration ( tryLock (long, TimeUnit) ).
  3. Une classe Lock peut également fournir un comportement et une sémantique assez différents de ceux du verrouillage de moniteur implicite, tels que

    .
    1. commande garantie
    2. utilisation par les non-participants
    3. Détection des interblocages

Consultez cette question de la SE concernant différents types de Serrures :

Synchronization vs Lock

Vous pouvez assurer la sécurité des threads en utilisant une API de simultanéité avancée au lieu de blocs synchronisés. Cette documentation fournie par la page bonnes constructions de programmation pour assurer la sécurité des threads.

Objets de verrouillage prend en charge les expressions de verrouillage qui simplifient de nombreuses applications simultanées.

Exécuteurs une API de haut niveau pour le lancement et la gestion des threads. Les implémentations d'exécuteur fournies par java.util.concurrent fournissent une gestion de pool de threads adaptée aux applications à grande échelle.

Collections simultanées simplifier la gestion de grandes collections de données et réduire considérablement le besoin de synchronisation.

Variables atomiques disposent de fonctionnalités qui minimisent la synchronisation et permettent d'éviter les erreurs de cohérence de la mémoire.

ThreadLocalRandom (dans JDK 7) permet de générer efficacement des nombres pseudo-aléatoires à partir de plusieurs threads.

Consultez la section java.util. .concurrent et java.util.concurrent.atomic également pour d’autres constructions de programmation.

Si vous avez décidé que:

  • ce que vous devez faire est de verrouiller l'objet actuel; et
  • tu veux verrouillez-le avec une granularité inférieure à une méthode complète;

alors je ne vois pas le tabou sur synchronizezd (this).

Certaines personnes utilisent délibérément synchronisé (ceci) (au lieu de marquer la méthode comme synchronisée) dans tout le contenu d'une méthode, car elles pensent que cela est "plus clair pour le lecteur". quel objet est en train d'être synchronisé. Tant que les gens font un choix éclairé (par exemple, comprenez que, ce faisant, ils insèrent en fait des bytecodes supplémentaires dans la méthode et que cela pourrait avoir des répercussions sur les optimisations potentielles), je ne vois pas de problème particulier avec cela. . Vous devez toujours documenter le comportement simultané de votre programme. Par conséquent, je ne vois pas le " '' synchronisé '' publier le comportement " argument comme étant si convaincant.

En ce qui concerne la question de savoir quel verrou d'objet vous devez utiliser, je pense qu'il n'y a rien de mal à synchroniser sur l'objet actuel si cela est attendu par la logique de ce que vous faites et comment votre classe serait typiquement utilisé . Par exemple, avec une collection, l’objet que vous vous attendez logiquement à verrouiller est généralement la collection elle-même.

Je pense qu’il existe une bonne explication de la raison pour laquelle chacune de ces techniques est vitale dans un livre intitulé "Concurrence Java en pratique" de Brian Goetz. Il précise un point: vous devez utiliser le même verrou "EVERYWHERE". pour protéger l'état de votre objet. La méthode synchronisée et la synchronisation sur un objet vont souvent de pair. Par exemple. Vector synchronise toutes ses méthodes. Si vous avez un handle sur un objet vectoriel et que vous allez faire "put if absent" alors simplement que Vector synchronise ses propres méthodes ne va pas vous protéger de la corruption de l'Etat. Vous devez synchroniser en utilisant synchronized (vectorHandle). Cela aura pour conséquence que le verrou SAME sera acquis par chaque thread ayant un handle sur le vecteur et protégera l'état général du vecteur. Ceci est appelé verrouillage côté client. Nous savons en effet que le vecteur synchronise (ceci) / synchronise toutes ses méthodes et que, par conséquent, la synchronisation sur l'objet vectorHandle entraînera une synchronisation correcte de l'état des objets vectoriels. Son idiot de croire que vous êtes thread-safe juste parce que vous utilisez une collection thread-safe. C’est précisément la raison pour laquelle ConcurrentHashMap a explicitement introduit la méthode putIfAbsent - pour rendre de telles opérations atomiques.

En résumé

  1. La synchronisation au niveau de la méthode permet le verrouillage côté client.
  2. Si vous avez un objet de verrouillage privé, il devient impossible de verrouiller le côté client. C’est parfait si vous savez que votre classe n’a pas " mis en cas d’absent " type de fonctionnalité.
  3. Si vous concevez une bibliothèque, la synchronisation sur cette méthode ou sur la méthode est souvent plus sage. Parce que vous êtes rarement en mesure de décider de l’utilisation de votre classe.
  4. Si Vector avait utilisé un objet de verrou privé, il aurait été impossible d'obtenir "placé si absent". droite. Le code client ne gagnera jamais le verrou privé, enfreignant ainsi la règle fondamentale consistant à utiliser EXACT SAME LOCK pour protéger son état.
  5. La synchronisation sur cette méthode ou sur les méthodes synchronisées pose un problème, comme d'autres l'ont déjà souligné: quelqu'un pourrait obtenir un verrou et ne jamais le libérer. Tous les autres threads continueraient à attendre que le verrou soit libéré. ??
  6. Alors, sachez ce que vous faites et adoptez celui qui est correct.
  7. Quelqu'un a fait valoir qu'avoir un objet de verrou privé vous donne une meilleure granularité - par exemple. si deux opérations ne sont pas liées, elles pourraient être protégées par des verrous différents, ce qui permettrait d'améliorer le débit. Mais ceci, je pense, est une odeur de conception et non une odeur de code - si deux opérations sont complètement indépendantes, pourquoi font-elles partie de la classe SAME? Pourquoi un club de classe devrait-il avoir des fonctionnalités sans rapport? Peut-être une classe utilitaire? Hmmmm - certains util, fournissant une manipulation de chaîne et un format de date de calendrier à travers la même instance ?? ... n'a pas de sens pour moi au moins !!

Non, vous ne devriez pas toujours . Cependant, j'ai tendance à l'éviter lorsqu'il y a de multiples préoccupations sur un objet particulier qui doivent seulement être thread-safe par rapport à elles-mêmes. Par exemple, vous pouvez avoir un objet de données mutable ayant le libellé "label". et " parent " des champs; ceux-ci doivent être threadsafe, mais changer l'un n'empêche pas l'autre d'être écrit / lu. (En pratique, j’éviterais cela en déclarant les champs volatiles et / ou en utilisant les wrappers AtomicFoo de java.util.concurrent).

La synchronisation en général est un peu maladroite, car elle gêne fortement le verrouillage au lieu de se demander comment les threads pourraient être autorisés à se contourner. Utiliser synchronized (this) est encore plus maladroit et antisocial, car cela signifie que "personne ne peut changer quoi que ce soit sur cette classe tant que je garde le verrou". À quelle fréquence avez-vous réellement besoin de le faire?

Je préférerais de beaucoup avoir des serrures plus granulaires; même si vous voulez tout empêcher de changer (peut-être que vous sérialisez l'objet), vous pouvez simplement acquérir tous les verrous pour obtenir la même chose, et en plus, c'est plus explicite. Lorsque vous utilisez synchronized (this) , vous ne comprenez pas exactement pourquoi vous synchronisez ni quels en sont les effets secondaires. Si vous utilisez synchronized (labelMonitor) , ou mieux encore labelLock.getWriteLock (). Lock () , il est clair que vous faites et quels sont les effets de votre section critique limité à.

Réponse courte : vous devez comprendre la différence et choisir en fonction du code.

Réponse longue : en général, je préférerais éviter de synchroniser > pour réduire les conflits, mais les verrous privés ajoutent une complexité dont vous devez être conscient. Utilisez donc la bonne synchronisation pour le bon travail. Si vous n'êtes pas aussi expérimenté en programmation multi-thread, je préférerais m'en tenir au verrouillage d'instance et lire sur ce sujet. (Cela dit: le simple fait d'utiliser synchronize (this) ne rend pas automatiquement votre classe totalement thread-safe.) Ce n'est pas un sujet facile, mais une fois que vous vous y êtes habitué, la réponse est de savoir si < em> synchronize (this) ou non vient naturellement.

Un verrou sert à la visibilité ou à la protection de certaines données contre une modification simultanée susceptible de conduire à la course.

Lorsque vous souhaitez simplement que les opérations de type primitif soient atomiques, des options telles que AtomicInteger et les "likes" sont disponibles.

Mais supposons que vous ayez deux nombres entiers liés l'un à l'autre, tels que les coordonnées x et y , qui sont liés l'un à l'autre et doivent être modifiés de manière atomique. manière. Ensuite, vous les protégeriez avec le même verrou.

Un verrou ne doit protéger que l’État lié entre eux. Pas moins et pas plus. Si vous utilisez synchronized (this) dans chaque méthode, même si l'état de la classe n'est pas lié, tous les threads seront confrontés au conflit, même si vous mettez à jour un état non lié.

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;
   }
}

Dans l'exemple ci-dessus, je n'ai qu'une méthode qui mute à la fois x et y et non deux méthodes différentes comme x et y sont liés et si j'avais donné deux méthodes différentes pour muter x et y séparément, le thread ne serait pas sécurisé.

Cet exemple est juste pour démontrer et pas nécessairement la façon dont il devrait être mis en œuvre. La meilleure façon de le faire serait de le rendre IMMUTABLE .

Maintenant, contrairement à l'exemple Point , il existe un exemple de TwoCounters déjà fourni par @Andreas où l'état qui est protégé par deux verrous différents en tant qu'état sans lien l'un avec l'autre.

Le processus d'utilisation de différents verrous pour protéger des états non liés s'appelle Verrouiller la séparation ou le fractionnement de verrou

La raison pour ne pas synchroniser sur ceci est que vous avez parfois besoin de plus d'un verrou (le deuxième verrou est souvent supprimé après une réflexion supplémentaire, mais vous en avez toujours besoin à l'état intermédiaire). Si vous verrouillez this , vous devez toujours vous rappeler lequel des deux verrous est this ; si vous verrouillez un objet privé, le nom de la variable vous l'indique.

Du point de vue du lecteur, si vous voyez le verrouillage sur this , vous devez toujours répondre aux deux questions suivantes:

  1. quel type d'accès est protégé par this ?
  2. un verrou suffit-il vraiment, quelqu'un n'a-t-il pas introduit un bogue?

Un exemple:

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(...);
        ...
    }
}

Si deux threads commencent par longOperation () sur deux instances différentes de BadObject , ils acquièrent leurs serrures; lorsqu'il est temps d'invoquer l.onMyEvent (...) , nous sommes dans une impasse, car aucun des threads ne peut acquérir le verrou de l'autre objet.

Dans cet exemple, nous pouvons éliminer le blocage en utilisant deux verrous, un pour les opérations courtes et un pour les longs.

Comme déjà indiqué ici, le bloc synchronisé peut utiliser une variable définie par l'utilisateur comme objet verrou, lorsque la fonction synchronisée utilise uniquement "ceci". Et bien sûr, vous pouvez manipuler des zones de votre fonction qui doivent être synchronisées et ainsi de suite.

Mais tout le monde dit qu’il n’existe aucune différence entre la fonction synchronisée et le bloc qui couvre toute la fonction en utilisant "ceci". comme objet de verrouillage. Ce n'est pas vrai, la différence réside dans le code d'octet qui sera généré dans les deux situations. En cas d'utilisation synchronisée des blocs, il convient d'attribuer une variable locale contenant la référence à "this". Et comme résultat, nous aurons une taille de fonction un peu plus grande (inutile si vous n’avez que peu de fonctions).

Une explication plus détaillée de la différence que vous pouvez trouver ici: http://www.artima.com/insidejvm/ed2/threadsynchP.html

De plus, l'utilisation du bloc synchronisé n'est pas bonne en raison du point de vue suivant:

  

Le mot clé synchronized est très limité dans un domaine: lors de la sortie d'un bloc synchronisé, tous les threads en attente de ce verrou doivent être débloqués, mais un seul de ces threads est autorisé à le verrouiller; tous les autres voient que le verrou est pris et reviennent à l'état bloqué. Ce n’est pas simplement une perte de cycles de traitement inutiles: souvent, le changement de contexte pour débloquer un fil implique également de paginer de la mémoire sur le disque, ce qui est très, très coûteux.

Pour plus de détails dans ce domaine, je vous recommande de lire cet article: http://java.dzone.com/articles/synchronized-considered

Ceci est vraiment juste un complément aux autres réponses, mais si votre principale objection à l'utilisation d'objets privés pour le verrouillage est qu'elle encombre votre classe avec des champs qui ne sont pas liés à la logique métier, Project Lombok a @Synchronized pour générer le passe-partout au moment de la compilation:

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

compile pour

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

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

Un bon exemple d'utilisation synchronisée (this).

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

Comme vous pouvez le voir ici, nous utilisons synchronize sur ceci pour coopérer plus ou moins longtemps (éventuellement avec une boucle infinie de la méthode d’exécution) avec certaines méthodes synchronisées.

Bien sûr, il peut être très facilement réécrit avec l’utilisation de synchronized on private field. Mais parfois, lorsque nous avons déjà des conceptions avec des méthodes synchronisées (c’est-à-dire, la classe héritée sur laquelle on dérive, synchronized (ceci peut être la seule solution).

Cela dépend de la tâche que vous souhaitez effectuer, mais je ne l'utiliserais pas. Aussi, vérifiez si le thread-save-ness que vous voulez accompagner ne pourrait pas être fait en synchronisant (ceci) en premier lieu? Il existe également quelques verrous dans l’API qui pourrait vous aider:)

Je veux seulement mentionner une solution possible pour des références privées uniques dans des parties atomiques de code sans dépendances. Vous pouvez utiliser un tableau de hachage statique avec des verrous et une méthode statique simple appelée atomic () qui crée automatiquement les références requises à l'aide des informations de pile (nom de classe complet et numéro de ligne). Vous pouvez ensuite utiliser cette méthode pour synchroniser des instructions sans écrire de nouvel objet verrou.

// 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
}

Évitez d'utiliser synchronized (this) comme mécanisme de verrouillage: cela verrouille l'instance de classe entière et peut provoquer des blocages. Dans de tels cas, refactorisez le code pour verrouiller uniquement une méthode ou une variable spécifique, afin que toute la classe ne soit pas verrouillée. Synchronized peut être utilisé dans le niveau de la méthode.
Au lieu d'utiliser synchronized (this) , le code ci-dessous montre comment verrouiller une méthode.

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

Mes deux cents en 2019, même si cette question aurait déjà pu être réglée.

Verrouiller 'ceci' n'est pas mauvais si vous savez ce que vous faites mais derrière la scène verrouiller 'ceci' l'est (ce que malheureusement le mot clé synchronisé dans la définition de méthode permet).

Si vous voulez réellement que les utilisateurs de votre classe puissent "voler" votre verrou (c'est-à-dire empêcher les autres threads de le gérer), vous voulez que toutes les méthodes synchronisées attendent pendant qu'une autre méthode de synchronisation est en cours d'exécution, etc. Il doit être intentionnel et bien pensé (et donc documenté pour aider vos utilisateurs à le comprendre).

Pour préciser, inversement, vous devez savoir ce que vous gagnez (ou perdez) si vous verrouillez un verrou non accessible (personne ne peut "voler" votre verrou, vous êtes totalement bientôt...).

Le problème pour moi est que le mot clé synchronisé dans la signature de définition de méthode rend trop facile pour les programmeurs de ne pas réfléchir à ce qu'il faut verrouiller, ce qui est une chose extrêmement importante à laquelle penser. ne voulez pas rencontrer de problèmes dans un programme multi-thread.

On ne peut pas dire que vous ne voulez généralement pas que les utilisateurs de votre classe puissent faire ce genre de choses ou ce que vous voulez généralement ... Cela dépend de la fonctionnalité que vous codez. Vous ne pouvez pas créer de règle du pouce car vous ne pouvez pas prédire tous les cas d'utilisation.

Considérez par exemple le graveur qui utilise un verrou interne, mais les utilisateurs ont du mal à l’utiliser à partir de plusieurs threads s’ils ne veulent pas que leurs résultats soient entrelacés.

Si votre cadenas est accessible en dehors de la classe, votre décision en tant que programmeur dépend des fonctionnalités de la classe. Cela fait partie de l'api. Par exemple, vous ne pouvez pas vous écarter par exemple de synchronisé (ceci) en synchronisé (provateObjet) sans risquer de modifier radicalement le code en l’utilisant.

Note 1: Je sais que vous pouvez réaliser tout ce que vous synchronisez (ceci) "réalise" en utilisant un objet de verrouillage explicite et en l'exposant, mais je pense qu'il est inutile si votre comportement est bien documenté et que vous savez réellement ce que verrouiller sur "ceci" signifie.

Note 2: Je ne partage pas l’argument selon lequel si du code vole votre verrou accidentellement, c’est un bogue et vous devez le résoudre. En un sens, c'est le même argument que de dire que je peux rendre toutes mes méthodes publiques même si elles ne sont pas censées être publiques. Si quelqu'un appelle «accidentellement» ma méthode d'intention privée, c'est un bogue. Pourquoi activer cet accident en premier lieu !!! Si la capacité de voler votre verrou est un problème pour votre classe, ne le permettent pas. Aussi simple que cela.

Je pense que les points un (quelqu'un d'autre utilisant votre verrou) et deux (toutes les méthodes utilisant le même verrou sans nécessité) peuvent se produire dans n'importe quelle application assez grosse. Surtout quand il n'y a pas de bonne communication entre les développeurs.

Cela n’est pas gravé dans le marbre, c’est surtout une question de bonne pratique et de prévention des erreurs.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top