Évitez synchronisé (ce) en Java?

381

Chaque fois qu'une question surgit sur SO à propos de la synchronisation Java, certaines personnes sont très désireuses de souligner que cela synchronized(this)devrait être évité. Au lieu de cela, affirment-ils, un verrou sur une référence privée doit être préféré.

Certaines des raisons données sont:

D'autres personnes, dont moi, soutiennent que synchronized(this)c'est un idiome qui est beaucoup utilisé (également dans les bibliothèques Java), est sûr et bien compris. Il ne faut pas l'éviter car vous avez un bug et vous n'avez pas la moindre 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é à voir des exemples du monde réel (pas de trucs de foobar) où éviter un verrouillage thisest préférable quand synchronized(this)ferait également le travail.

Par conséquent: devez-vous toujours l'éviter synchronized(this)et le remplacer par un verrou sur une référence privée?


Quelques informations supplémentaires (mises à jour au fur et à mesure des réponses):

  • nous parlons de synchronisation d'instance
  • les synchronizedformes implicites ( méthodes) et explicites de synchronized(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 Java efficace, é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 celle synchronized(this)fournie, alors ce synchronized(this)n'est pas applicable donc ce n'est pas le problème
eljenso
la source
4
Je voudrais également souligner que le contexte est important - le bit "Typiquement, c'est le verrou sur l'instance elle-même" se trouve dans une section sur la documentation d'une classe conditionnellement thread-safe, lorsque vous rendez le verrou public. En d'autres termes, cette phrase s'applique lorsque vous avez déjà pris cette décision.
Jon Skeet
En l'absence de synchronisation interne, et lorsqu'une synchronisation externe est nécessaire, le verrou est souvent l'instance elle-même, explique Bloch. Alors, pourquoi ne serait-ce pas le cas pour la synchronisation interne avec verrouillage sur «ceci» également? (L'importance de la documentation est un autre problème.)
eljenso
Il y a un compromis entre la granularité étendue et la surcharge du cache CPU et des demandes de bus, car le verrouillage sur un objet externe nécessitera très probablement une ligne de cache séparée pour être modifiée et échangée entre les caches CPU (cf. MESIF et MOESI).
ArtemGr
1
Je pense que dans le monde de la programmation défensive, vous évitez les bugs non pas par idiome mais par code. Lorsque quelqu'un me pose une question, "Dans quelle mesure votre synchronisation est-elle optimisée?", Je veux dire "Très" au lieu de "Très, sauf si quelqu'un d'autre ne suit pas l'idiome".
Sgene9

Réponses:

132

Je couvrirai chaque point séparément.

  1. Un code maléfique peut voler votre serrure (très populaire, il a également une variante "accidentellement")

    Je suis plus inquiet par accident . Cela revient à dire que cette utilisation de thisfait 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. Cela est vrai pour des choses comme Collections.synchronizedMap(voir le javadoc).

  2. Toutes les méthodes synchronisées au sein de la même classe utilisent exactement le même verrou, 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 appropriée pour le 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 de cette exposition, ne le faites pas.

Darron
la source
1. "synchronisé" ne fait pas partie de l'interface exposée de votre classe. 2. d'accord 3. voir 1.
eljenso
66
Essentiellement synchronisé (ceci) est exposé car cela signifie que le code externe peut affecter le fonctionnement de votre classe. J'affirme donc que vous devez le documenter en tant qu'interface, même si la langue ne le fait pas.
Darron
15
Similaire. Voir Javadoc pour Collections.synchronizedMap () - l'objet renvoyé utilise synchronized (this) en interne et ils s'attendent à ce que le consommateur en profite pour utiliser le même verrou pour les opérations atomiques à grande échelle comme l'itération.
Darron
3
En fait, Collections.synchronizedMap () n'utilise PAS synchronisé (ceci) en interne, il utilise un objet de verrouillage final privé.
Bas Leijdekkers
1
@Bas Leijdekkers: la documentation spécifie clairement que la synchronisation se produit sur l'instance de carte retournée. Ce qui est intéressant, c'est que les vues retournées par keySet()et values()ne se verrouillent pas sur (leur) this, mais l'instance de carte, qui est importante pour obtenir un comportement cohérent pour toutes les opérations de carte. La raison pour laquelle l'objet verrou est pris en compte dans une variable est que la sous-classe en a SynchronizedSortedMapbesoin pour implémenter des sous-cartes qui se verrouillent sur l'instance de carte d'origine.
Holger
86

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
}

ce qui est une raison de ne pas l'utiliser synchronized(this). Vous pourriez faire valoir que vous pouvez faire des choses autour du synchronized(this)bloc. La raison habituelle est d'essayer et d'éviter d'avoir à faire la vérification synchronisée, ce qui entraîne toutes sortes de problèmes de concurrence, en particulier le problème de verrouillage à double vérification , qui montre à quel point il peut être difficile de faire une vérification relativement simple threadsafe.

Une serrure privée est un mécanisme défensif, ce qui n'est jamais une mauvaise idée.

De plus, comme vous l'avez mentionné, les verrous privés peuvent contrôler la granularité. Un ensemble d'opérations sur un objet peut être totalement indépendant d'un autre mais synchronized(this)exclura mutuellement l'accès à tous.

synchronized(this) ne vous donne vraiment rien.

cletus
la source
4
"synchronisé (cela) ne vous donne vraiment rien." Ok, je le remplace par une synchronisation (myPrivateFinalLock). Qu'est-ce que ça me donne? Vous dites que c'est un mécanisme défensif. Contre quoi suis-je protégé?
eljenso
14
Vous êtes protégé contre le verrouillage accidentel (ou malveillant) de «ceci» par des objets externes.
cletus
14
Je ne suis pas du tout d'accord avec cette réponse: un verrou doit toujours être maintenu aussi longtemps que possible, et c'est précisément la raison pour laquelle vous voudriez "faire des choses" autour d'un bloc synchronisé au lieu de synchroniser toute la méthode .
Olivier
5
Faire des choses en dehors du bloc synchronisé est toujours bien intentionné. Le fait est que les gens se trompent la plupart du temps et ne s'en rendent même pas compte, tout comme dans le problème de verrouillage à double vérification. La route de l'enfer est pavée de bonnes intentions.
cletus le
16
Je ne suis pas d'accord en général avec "X est un mécanisme défensif, ce qui n'est jamais une mauvaise idée." Il y a beaucoup de code inutilement gonflé à cause de cette attitude.
finnw
54

Lorsque vous utilisez synchronisé (ceci), vous utilisez l'instance de classe comme verrou lui-même. Cela signifie que si le verrouillage est acquis par le thread 1 , le thread 2 doit attendre.

Supposons que le code suivant:

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


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

La méthode 1 modifiant la variable a et la méthode 2 modifiant la variable b , la modification simultanée de la même variable par deux threads doit être évitée et elle l'est. MAIS tout en modifiant thread1 a et en modifiant thread2 b, cela peut être effectué 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 dans une condition de concurrence critique, doivent attendre et, évidemment, le code sacrifie la concurrence du programme.

La solution consiste à utiliser 2 verrous différents 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, par conséquent, permet une meilleure concurrence, en revanche, il est devenu plus complexe que le premier exemple ...

Andreas Bakurov
la source
C'est très dangereux. Vous avez maintenant introduit une exigence de commande de verrouillage côté client (utilisateur de cette classe). Si deux threads appellent method1 () et method2 () dans un ordre différent, ils risquent de se bloquer, mais l'utilisateur de cette classe n'a aucune idée que c'est le cas.
daveb
7
La granularité non fournie par "synchronisé (ceci)" est hors de portée de ma question. Et vos champs de verrouillage ne devraient-ils pas être définitifs?
eljenso
10
afin d'avoir une impasse, nous devrions effectuer un appel du bloc synchronisé par A au bloc synchronisé par B. daveb, vous vous trompez ...
Andreas Bakurov
3
Il n'y a pas de blocage dans cet exemple pour autant que je puisse voir. J'accepte qu'il ne s'agit que d'un pseudocode mais j'utiliserais l'une des implémentations de java.util.concurrent.locks.Lock comme java.util.concurrent.locks.ReentrantLock
Shawn Vader
15

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

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

La version "accidentelle" semble moins probable, mais comme on dit, "faites quelque chose d'idiot-proof et quelqu'un inventera un meilleur idiot".

Je suis donc d'accord avec l'école de pensée qui dépend de ce que la classe fait.


Modifier les 3 premiers commentaires d'eljenso suivants:

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

Supposons que votre système soit un conteneur de servlet et que nous considérions l' ServletContextimplémentation. Sa getAttributeméthode doit être thread-safe, car les attributs de contexte sont des données partagées; vous le déclarez donc synchronized. Imaginons également que vous fournissiez un service d'hébergement public basé sur l'implémentation de votre conteneur.

Je suis votre client et déploie ma "bonne" servlet sur votre site. Il arrive que mon code contienne 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 initméthode:

synchronized (this.getServletConfig (). getServletContext ()) {
   tandis que (vrai) {}
}

En supposant que nous partageons 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 getAttributeest verrouillé pour toujours. Le pirate a réalisé un DoS sur ma servlet.

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

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

Je ferais donc mon choix de conception en fonction de la sécurité: vais-je avoir un contrôle complet sur le code qui a accès aux instances? Quelle serait la conséquence d'un thread qui détient un verrou sur une instance indéfiniment?

Olivier
la source
cela dépend de ce que fait la classe: s'il s'agit d'un objet «important», alors verrouiller la référence privée? Sinon, le verrouillage d'instance suffira?
eljenso
6
Oui, le scénario de vol de serrure me semble bien tiré par les cheveux. Tout le monde le mentionne, mais qui l'a réellement fait ou vécu? Si vous verrouillez "accidentellement" un objet que vous ne devriez pas, alors il y a un nom pour ce type de situation: c'est un bug. Répare le.
eljenso
4
De plus, le verrouillage des références internes n'est pas exempt de «l'attaque de synchronisation externe»: si vous savez qu'une certaine partie synchronisée du code attend qu'un événement externe se produise (par exemple, écriture de fichier, valeur dans la base de données, événement de minuterie), vous pouvez probablement faites en sorte qu'il se bloque également.
eljenso
Permettez-moi d'avouer que je suis l'un de ces idiots, bien que je l'ai fait quand j'étais jeune. Je pensais que le code était plus propre en ne créant pas d'objet de verrouillage explicite et, à la place, j'utilisais un autre objet final privé qui devait participer au moniteur. Je ne savais pas que l'objet lui-même se synchronisait sur lui-même. Vous pouvez imaginer le hijinx qui s'ensuit ...
Alan Cabrera
12

Ça dépend de la situation.
S'il n'y a qu'une seule entité de partage ou plus d'une.

Voir l'exemple de travail complet ici

Une petite introduction.

Threads et entités partageables
Il est possible pour plusieurs threads d'accéder à la même entité, par exemple pour plusieurs connexionsThreads partageant un même messageQueue. Étant donné que les threads s'exécutent simultanément, il peut y avoir une possibilité de remplacer ses données par une autre, ce qui peut être une situation foireuse.
Nous avons donc besoin d'un moyen de garantir que l'entité partageable n'est accessible que par un thread à la fois. (CONCURRENCE).

Bloc
synchronisé Le bloc synchronized () est un moyen d'assurer un accès simultané à une entité partageable.
Tout d'abord, une petite analogie
Supposons qu'il y ait deux personnes P1, P2 (fils) un lavabo (entité partageable) à l'intérieur d'une salle de bain et qu'il y ait une porte (serrure).
Maintenant, nous voulons qu'une personne utilise le lavabo à la fois.
Une approche consiste à verrouiller la porte par P1 lorsque la porte est verrouillée P2 attend jusqu'à ce que p1 termine son travail
P1 déverrouille la porte
puis seul p1 peut utiliser le lavabo.

syntaxe.

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

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

washbasin1;  
washbasin2;

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

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

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

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

Rohit Singh
la source
11

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 sans doute plus sûr:

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

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

Cela ne signifie pas une fouille contre Java, juste un reflet de mon expérience de travail sur les deux langages.

serg10
la source
3
Peut-être que C # étant un langage plus jeune, ils ont appris de mauvais schémas qui ont été compris dans le camp Java et codé mieux comme ça? Y a-t-il aussi moins de singletons? :)
Bill K
3
Il lui. Peut-être vrai, mais je ne vais pas monter à l'appât! Je pense que je peux dire avec certitude qu'il y a plus de majuscules dans le code C #;)
serg10
1
Tout simplement pas vrai (pour le dire joliment)
tcurdt
7

Le java.util.concurrentpackage a considérablement réduit la complexité de mon code thread-safe. Je n'ai que des preuves anecdotiques pour continuer, mais la plupart des travaux que j'ai vus synchronized(x)semblent réimplémenter un verrou, un sémaphore ou un verrou, mais en utilisant les moniteurs de niveau inférieur.

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 de laisser échapper un verrou. Ceci est avantageux dans la mesure où vous avez la certitude absolue que vous contrôlez l'entrée dans le moniteur par deux threads ou plus.

jamesh
la source
6
  1. Rendez vos données immuables si possible ( finalvariables)
  2. Si vous ne pouvez pas éviter la mutation des données partagées sur plusieurs threads, utilisez des constructions de programmation de haut niveau [par exemple, LockAPI granulaire ]

Un verrou offre 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 ReentrantLockqui implémente l' Lockinterface

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

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

Avantages de Lock over Synchronized (ceci)

  1. L'utilisation de méthodes ou d'instructions synchronisées oblige toutes les acquisitions et libérations de verrous à se produire de manière structurée par blocs.

  2. Les implémentations de verrouillage offrent 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'acquérir le verrou qui peut être interrompu ( lockInterruptibly())
    3. Une tentative d'acquérir le verrou qui peut expirer ( tryLock(long, TimeUnit)).
  3. Une classe Lock peut également fournir un comportement et une sémantique très différents de ceux du verrouillage implicite du moniteur, tels que

    1. commande garantie
    2. utilisation non réentrante
    3. Détection de blocage

Jetez un oeil à cette question SE concernant différents types de Locks:

Synchronisation vs verrouillage

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

Les objets de verrouillage prennent en charge les idiomes de verrouillage qui simplifient de nombreuses applications simultanées.

Les exécuteurs définissent une API de haut niveau pour lancer et gérer les 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.

Les collections simultanées facilitent la gestion de grandes collections de données et peuvent considérablement réduire le besoin de synchronisation.

Les variables atomiques ont des fonctionnalités qui minimisent la synchronisation et aident à éviter les erreurs de cohérence de la mémoire.

ThreadLocalRandom (dans JDK 7) fournit une génération efficace de nombres pseudo-aléatoires à partir de plusieurs threads.

Reportez-vous également aux packages java.util.concurrent et java.util.concurrent.atomic pour d'autres constructions de programmation.

Ravindra babu
la source
5

Si vous avez décidé que:

  • la chose que vous devez faire est de verrouiller l'objet en cours; et
  • vous voulez le verrouiller avec une granularité plus petite qu'une méthode entière;

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

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 parce qu'elles pensent qu'il est "plus clair pour le lecteur" sur quel objet est réellement synchronisé. Tant que les gens font un choix éclairé (par exemple, comprenez qu'en faisant cela, ils insèrent en fait des bytecodes supplémentaires dans la méthode et cela pourrait avoir un effet d'entraînement sur les optimisations potentielles), je ne vois pas particulièrement de problème avec cela . Vous devez toujours documenter le comportement simultané de votre programme, donc je ne vois pas l'argument "'synchronisé' publie le comportement" comme étant si convaincant.

En ce qui concerne la question de 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 généralement utilisée . Par exemple, avec une collection, l'objet que vous vous attendez logiquement à verrouiller est généralement la collection elle-même.

Neil Coffey
la source
1
"si cela pouvait être attendu par la logique ..." est un point que j'essaie de faire passer également. Je ne vois pas l'intérêt d' utiliser toujours des verrous privés, bien que le consensus général semble être que c'est mieux, car cela ne fait pas de mal et est plus défensif.
eljenso
4

Je pense qu'il y a une bonne explication sur la raison pour laquelle chacune de ces techniques est vitale à votre actif dans un livre intitulé Java Concurrency In Practice de Brian Goetz. Il fait une remarque très claire - vous devez utiliser le même verrou "PARTOUT" 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 une poignée sur un objet vectoriel et que vous allez faire "mettre si absent", alors simplement la synchronisation vectorielle de ses propres méthodes individuelles ne vous protégera pas de la corruption d'état. Vous devez synchroniser en utilisant synchronized (vectorHandle). Cela entraînera l'acquisition du même verrou par chaque thread qui possède une poignée pour le vecteur et protégera l'état général du vecteur. C'est ce qu'on appelle le verrouillage côté client. Nous savons en fait que le vecteur synchronise (ceci) / synchronise toutes ses méthodes et donc la synchronisation sur l'objet vectorHandle entraînera une synchronisation correcte de l'état des objets vectoriels. Il est insensé de croire que vous êtes thread-safe uniquement 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é - cela rend le verrouillage côté client impossible. C'est très bien si vous savez que votre classe n'a pas de fonctionnalité de type "mettre si absent".
  3. Si vous concevez une bibliothèque, alors la synchronisation ou la synchronisation de la méthode est souvent plus sage. Parce que vous êtes rarement en mesure de décider comment votre classe va être utilisée.
  4. Si Vector avait utilisé un objet de verrouillage privé - il aurait été impossible d'obtenir le droit de "mettre s'il était absent". Le code client ne gagnera jamais la poignée du verrou privé, enfreignant ainsi la règle fondamentale de l'utilisation du MÊME VERROU EXACT pour protéger son état.
  5. La synchronisation sur cette méthode ou sur les méthodes synchronisées a un problème, comme d'autres l'ont souligné - quelqu'un pourrait obtenir un verrou et ne jamais le libérer. Tous les autres threads continueraient d'attendre la libération du verrou.
  6. Alors sachez ce que vous faites et adoptez celui qui est correct.
  7. Quelqu'un a fait valoir que le fait d'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 résultant en un meilleur débit. Mais je pense que c'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 même classe? Pourquoi un club de classe ne devrait-il pas avoir de fonctionnalités indépendantes? Peut être une classe utilitaire? Hmmmm - certains utilisent la manipulation de chaînes et le formatage de la date du calendrier via la même instance ?? ... n'a pas de sens pour moi au moins !!
Sandesh Sadhale
la source
3

Non, tu ne devrais pas toujours . Cependant, j'ai tendance à l'éviter lorsqu'il y a plusieurs préoccupations sur un objet particulier qui n'ont besoin que d'être threadsafe en ce qui les concerne. Par exemple, vous pourriez avoir un objet de données mutable qui a des champs "label" et "parent"; ceux-ci doivent être threadsafe, mais changer l'un ne doit pas empêcher l'autre d'être écrit / lu. (En pratique, j'éviterais cela en déclarant les champs volatils et / ou en utilisant les wrappers AtomicFoo de java.util.concurrent).

La synchronisation en général est un peu maladroite, car elle claque un gros verrou plutôt que de penser exactement comment les threads pourraient être autorisés à fonctionner les uns avec les autres. L'utilisation synchronized(this)est encore plus maladroite et antisociale, car elle dit "personne ne peut rien changer à cette classe pendant que je tiens le verrou". À quelle fréquence avez-vous réellement besoin de faire cela?

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 c'est plus explicite de cette façon. Lorsque vous utilisez synchronized(this), vous ne savez pas exactement pourquoi vous effectuez la synchronisation ou quels pourraient être les effets secondaires. Si vous utilisez synchronized(labelMonitor), ou mieux encore labelLock.getWriteLock().lock(), il est clair ce que vous faites et à quoi se limitent les effets de votre section critique.

Andrzej Doyle
la source
3

Réponse courte : Vous devez comprendre la différence et faire un choix en fonction du code.

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

tcurdt
la source
Dois-je vous comprendre correctement lorsque vous dites que cela dépend de votre expérience?
eljenso
Tout d'abord, cela dépend du code que vous souhaitez écrire. Je dis juste que vous pourriez avoir besoin d'un peu plus d'expérience lorsque vous vous détournez pour ne pas utiliser la synchronisation (ceci).
tcurdt le
2

Un verrou est utilisé pour la visibilité ou pour protéger certaines données contre les modifications simultanées qui peuvent conduire à la course.

Lorsque vous avez juste besoin de faire des opérations de type primitif pour qu'elles soient atomiques, il existe des options disponibles comme AtomicIntegeret similaires.

Mais supposons que vous ayez deux entiers qui sont liés les uns aux autres comme xet les ycoordonnées, qui sont liées les unes aux autres et doivent être modifiées de manière atomique. Ensuite, vous les protégeriez à l'aide d'un même verrou.

Un verrou ne doit protéger que l'état qui est lié les uns aux autres. Ni moins ni 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 à des conflits, même s'ils mettent à 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 seule méthode qui mute à la fois xet ypas deux méthodes différentes en tant que xet ysont liées et si j'avais donné deux méthodes différentes pour muter xet yséparément, cela n'aurait pas été thread-safe.

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

Maintenant en opposition à l' Pointexemple, il y a un exemple de TwoCountersdéjà fourni par @Andreas où l'état qui est protégé par deux verrous différents car l'état n'est pas lié l'un à l'autre.

Le processus d'utilisation de différents verrous pour protéger des états indépendants s'appelle Lock Striping ou Lock Splitting

Narendra Pathai
la source
1

La raison de ne pas synchroniser sur ce point est que parfois vous avez besoin de plusieurs verrous (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 ceci , vous devez toujours vous rappeler lequel des deux verrous est présent ; si vous verrouillez un objet privé, le nom de la variable vous le dit.

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

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

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 longOperation()sur deux instances différentes de BadObject, ils acquièrent leurs verrous; quand il est temps d'appeler l.onMyEvent(...), nous avons un blocage 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 opérations longues.

18446744073709551615
la source
2
Le seul moyen d'obtenir un blocage dans cet exemple est lorsque BadObjectA invoque longOperationsur B, en passant A myListener, et vice versa. Pas impossible, mais assez compliqué, soutenant mes points précédents.
eljenso
1

Comme déjà dit ici, le bloc synchronisé peut utiliser une variable définie par l'utilisateur comme objet de verrouillage, 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, etc.

Mais tout le monde dit qu'il n'y a pas de 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 est dans le code d'octet qui sera généré dans les deux situations. En cas d'utilisation de bloc synchronisée, une variable locale doit être allouée qui contient une référence à "ceci". Et en conséquence, nous aurons une taille de fonction un peu plus grande (non pertinent 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 une zone: lorsque vous quittez un bloc synchronisé, tous les threads qui attendent ce verrou doivent être débloqués, mais un seul de ces threads peut prendre le verrou; tous les autres voient que le verrou est pris et reviennent à l'état bloqué. Ce n'est pas seulement beaucoup de cycles de traitement gaspillés: souvent, le changement de contexte pour débloquer un thread implique également la pagination de la mémoire sur le disque, et c'est très, très, cher.

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

romain
la source
1

C'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, alors Project Lombok doit @Synchronizedgénérer le passe-partout au moment de la compilation:

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

compile en

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

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

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

// 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 cela pour coopérer facilement de manière longue (éventuellement 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 en utilisant synchronisé sur un champ privé. Mais parfois, lorsque nous avons déjà une conception avec des méthodes synchronisées (c.-à-d. Classe héritée, nous dérivons de, synchronisé (ceci) peut être la seule solution).

Bart Prokop
la source
Tout objet pourrait être utilisé comme verrou ici. Ce n'est pas nécessaire this. Ce pourrait être un domaine privé.
finnw
Exact, mais le but de cet exemple était de montrer comment faire une bonne synchronisation, si nous décidions d'utiliser la synchronisation de méthode.
Bart Prokop
0

Cela dépend de la tâche que vous voulez faire, mais je ne l'utiliserais pas. De plus, vérifiez si la sauvegarde du thread que vous souhaitez accompagner ne peut pas être effectuée en synchronisant (ceci) en premier lieu? Il y a aussi de beaux verrous dans l'API qui pourraient vous aider :)

Harald Schilly
la source
0

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 Hashmap statique avec des verrous et une méthode statique simple nommée atomic () qui crée automatiquement les références requises en utilisant les informations de la pile (nom complet de la classe et numéro de ligne). Vous pouvez ensuite utiliser cette méthode pour synchroniser des instructions sans écrire de nouvel objet de verrouillage.

// 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
la source
0

É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, de cette façon, toute la classe ne sera pas verrouillée. Synchronisedpeut être utilisé au niveau de la méthode.
Au lieu d'utiliser synchronized(this), le code ci-dessous montre comment vous pouvez simplement verrouiller une méthode.

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

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

Le verrouillage sur «ceci» n'est pas mauvais si vous savez ce que vous faites mais derrière la scène, le verrouillage sur «ceci» est (ce que permet malheureusement le mot-clé synchronisé dans la définition de méthode).

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

Pour développer davantage, à l'inverse, vous devez savoir ce que vous `` gagnez '' (ou `` perdez '') si vous verrouillez un verrou non accessible (personne ne peut `` voler '' votre verrou, vous avez le contrôle total, etc.). ..).

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 penser à quoi verrouiller, ce qui est une chose très importante à penser si vous ne voulez pas rencontrer de problèmes dans un multi -programme fileté.

On ne peut pas affirmer que «généralement» vous ne voulez pas que les utilisateurs de votre classe puissent faire ces choses ou que «généralement» vous voulez ... Cela dépend de la fonctionnalité que vous codez. Vous ne pouvez pas faire une règle empirique car vous ne pouvez pas prédire tous les cas d'utilisation.

Considérez par exemple le rédacteur qui utilise un verrou interne mais les gens ont du mal à l'utiliser à partir de plusieurs threads s'ils ne veulent pas que leur sortie s'entrelace.

Si votre serrure est accessible en dehors de la classe ou non, c'est votre décision en tant que programmeur en fonction des fonctionnalités de la classe. Il fait partie de l'API. Vous ne pouvez pas passer par exemple de synchronisé (ceci) à synchronisé (provateObjet) sans risquer de casser les changements dans le code en l'utilisant.

Remarque 1: je sais que vous pouvez réaliser tout ce qui est synchronisé (ceci) en utilisant un objet de verrouillage explicite et en l'exposant, mais je pense que ce n'est pas nécessaire si votre comportement est bien documenté et que vous savez réellement ce que le verrouillage sur `` cela '' signifie.

Note 2: Je ne suis pas d'accord avec l'argument selon lequel si du code vole accidentellement votre verrou, c'est un bug et vous devez le résoudre. C'est en quelque sorte 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 destinée à être privée, c'est un bug. Pourquoi activer cet accident en premier lieu !!! Si la capacité de voler votre serrure est un problème pour votre classe, ne le permettez pas. Aussi simple que cela.

Amit Mittal
la source
-3

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

Ce n'est pas coulé dans le béton, c'est surtout une question de bonnes pratiques et de prévention des erreurs.

Yoni Roit
la source