web-dev-qa-db-fra.com

Évitez synchronisé (cela) en Java?

À chaque fois qu'une question se pose sur la synchronisation SO à propos de Java, certaines personnes sont très désireuses 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, soutiennent que synchronized(this) est un idiome utilisé fréquemment (également dans les bibliothèques Java,) est bien compris et sans danger. 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: si cela s'applique, utilisez-le.

Je suis intéressé à voir quelques exemples du monde réel (aucune substance foobar) où éviter un verrou sur this est préférable lorsque synchronized(this) ferait également le travail.

Par conséquent: devriez-vous toujours é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):

  • on parle de synchronisation d'instance
  • les deux formes implicites (synchronized) et la forme explicite de synchronized(this) sont considérées
  • si vous citez Bloch ou d'autres autorités sur le sujet, n'omettez pas les parties que vous n'aimez pas (par exemple, Effective Java, élément dans Thread Safety: ). 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 fournie par synchronized(this), alors synchronized(this) n'est pas applicable, ce n'est donc pas le problème
362
eljenso

Je couvrirai chaque point séparément.

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

    Je suis plus inquiet pour 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 de choses comme Collections.synchronizedMap (voir le javadoc).

  2. Toutes les méthodes synchronisées d'une 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 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.

124
Darron

Eh bien, premièrement, 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 des raisons pour ne pas utiliser synchronized(this). Vous pourriez faire valoir que vous pouvez faire des choses autour du bloc synchronized(this). La raison habituelle est d'essayer de ne pas avoir à effectuer de vérification synchrone, ce qui entraîne toutes sortes de problèmes de simultanéité, en particulier le problème de double verrouillage , ce qui montre bien à quel point il peut être difficile être de faire une vérification relativement simple threadsafe.

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

En outre, comme vous l'avez mentionné, 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 synchronized(this) exclura mutuellement l'accès à toutes.

synchronized(this) ne vous donne vraiment rien.

84
cletus

Pendant que vous utilisez synchronized (ceci), vous utilisez l'instance de classe comme un verrou lui-même. Cela signifie que tant que le verrou est acquis par thread 1, le thread 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 tandis que thread1 modifier a et thread2 modifier 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, devraient attendre et que le code sacrifie évidemment la concurrence du programme.

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

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, par contre il est devenu plus complexe que le premier exemple ...

52
Andreas Bakurov

Bien que je convienne de ne pas adhérer aveuglément aux règles dogmatiques, le scénario du "vol de serrure" vous semble-t-il si excentrique? Un thread pourrait en effet acquérir le verrou sur votre objet "en externe" (synchronized(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).

La version "accidentelle" semble moins probable, mais comme on dit, "faites quelque chose d'idiot-preuve 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.


Editez après les 3 premiers commentaires 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 thread-safe, car les attributs de contexte sont des données partagées; alors vous le déclarez comme synchronized. Imaginons également que vous fournissiez un service d'hébergement public basé sur votre implémentation de conteneur.

Je suis votre client et déploie 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:

 synchronisé (this.getServletConfig (). getServletContext ()) {
 tant que (vrai) {} 
} 

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 à 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 trop simpliste du fonctionnement d'un conteneur de servlets, mais à mon humble avis, cela prouve le point.

Je choisirais donc mon design en fonction de considérations de sécurité: aurais-je un 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?

14
Olivier

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 utiliser:

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

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 est devenue au fil du temps une version légèrement moins sûre, mais légèrement plus concise.

Ce n'est pas conçu comme un Dig contre Java, c'est juste un reflet de mon expérience de travail sur les deux langages.

11
serg10

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

Voir un 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 un même messageQueue. É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 l’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), un lavabo (entité à partager) à l'intérieur d'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 à l’aide de P1 lorsque la porte est verrouillée. P2 attend que p1 ait terminé son travail.
P1 ouvre la porte
alors seulement p1 peut utiliser un lavabo.

syntaxe.

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

"this" fournissait le verrou intrinsèque associé à la classe (classe Object conçue par le développeur Java de telle sorte que chaque objet puisse fonctionner en tant que moniteur). L'approche ci-dessus fonctionne bien lorsqu'il n'y a qu'une seule entité partagée et plusieurs threads (1: N).
enter image description here N entités partageables-M threads
Pensons maintenant à 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;
  }

enter image description here
enter image description here

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

10
Rohit Singh

Le paquetage Java.util.concurrent a considérablement réduit la complexité de mon code thread-safe. 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.

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 est bénéfique dans la mesure où vous avez la certitude absolue de contrôler l'entrée dans le moniteur par deux ou plusieurs threads.

7
jamesh
  1. Rendre vos données immuables si cela est possible (final variables)
  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. granulaire Lock API]

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 synchronisé (this)

  1. L'utilisation de méthodes ou d'instructions synchronisées force l'acquisition et la libération de tous les verrous d'une manière structurée en 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’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 assez différents de ceux du verrou de moniteur implicite, tels que

    1. commande garantie
    2. usage non re-entrant
    3. Détection de blocage

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

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 page fournit de bonnes constructions de programmation pour assurer la sécurité des threads.

Objets verrouillés prend en charge les idiomes de verrouillage qui simplifient de nombreuses applications simultanées.

Les exécuteurs définissent 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.

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

Variables atomiques ont des 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.

Reportez-vous aux packages Java.util.concurrent et Java.util.concurrent.atomic pour les autres constructions de programmation.

6
Ravindra babu

Si vous avez décidé que:

  • ce 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 (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 l'objectif "plus clair pour le lecteur" est réellement synchronisé. Tant que les gens font un choix éclairé (par exemple, comprenez que, ce faisant, ils insèrent en fait des codes supplémentaires dans la méthode et que cela pourrait avoir un effet d'entraînement sur les optimisations potentielles), je ne vois pas de problème particulier avec cela. . Vous devriez toujours documenter le comportement simultané de votre programme, donc je ne vois pas l'argument "'synchronisé' publie le comportement" comme étant aussi 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 par quoi votre classe serait normalement utilisée. Par exemple, avec une collection, l'objet que vous vous attendez logiquement à verrouiller est généralement la collection elle-même.

5
Neil Coffey

Je pense qu’il existe une bonne explication de la raison pour laquelle chacune d’elles est une technique vitale dans un livre intitulé Java Concurrency In Practice de Brian Goetz. Il précise un point très clairement: 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 un handle sur un objet vectoriel et que vous allez faire "put if absent", alors simplement, la synchronisation vectorielle avec ses propres méthodes individuelles ne va pas vous protéger de la corruption de l'état. 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é, le verrouillage côté client est impossible. Cela convient si vous savez que votre classe n'a pas de fonctionnalité "mis en cas d'absence".
  3. Si vous concevez une bibliothèque, la synchronisation sur cette méthode ou sur la méthode est souvent plus judicieuse. Parce que vous êtes rarement en mesure de décider de la façon dont votre classe sera utilisée.
  4. Si Vector avait utilisé un objet de verrouillage privé, il aurait été impossible d'obtenir le droit de "mettre si absent". 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 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 donnerait un meilleur débit. Mais je pense que c’est une odeur de conception et non une odeur de code - si deux opérations n’ont aucune relation, pourquoi font-elles partie de la même classe? Pourquoi un club de classe devrait-il avoir des fonctionnalités sans rapport? Peut-être une classe utilitaire? Hmmmm - certains util utilisent la manipulation de chaîne et le formatage de la date du calendrier par la même instance ... n'a aucun sens pour moi au moins !!
4
Sandesh Sadhale

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 des champs "label" et "parent"; ceux-ci doivent être threadsafe, mais changer l'un n'empêche pas l'autre d'être écrit/lu. (En pratique, je l’éviterais 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 bloque un gros verrou plutôt que de penser exactement à la manière dont les threads pourraient être contraints de se contourner. Utiliser synchronized(this) est encore plus maladroit et antisocial, car il dit "personne ne peut changer n'importe quoi sur cette classe tant que je garde 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 empêcher tout changement (peut-être que vous sérialisez l'objet), vous pouvez simplement acquérir tous les verrous pour obtenir la même chose, en plus c'est plus explicite. Lorsque vous utilisez synchronized(this), vous ne savez pas exactement pourquoi vous synchronisez ni quels sont les effets secondaires. Si vous utilisez synchronized(labelMonitor), ou mieux encore labelLock.getWriteLock().lock(), vous saurez clairement ce que vous faites et à quoi se limitent les effets de votre section critique.

3
Andrzej Doyle

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érerais plutôt éviter la synchronisation (ceci) pour réduire les conflits, mais les verrous privés ajoutent à la complexité que vous avez être au courant. 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 entièrement thread-safe.) Ce n'est pas un sujet facile, mais une s’y habituer, la réponse s’il faut utiliser synchroniser (ceci) ou ne pas venir naturellement.

3
tcurdt

Un verrou est utilisé soit pour visibilité , soit pour protéger certaines données de modifications simultanées pouvant conduire à la course.

Lorsque vous devez simplement faire en sorte que les opérations de type primitif soient atomiques, il existe des options telles que AtomicInteger et les préférences.

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

Un verrou ne devrait 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 est sans relation, tous les threads seront confrontés au conflit, même s'ils mettent à jour un état sans relation.

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ées et si j'avais donné deux méthodes différentes pour muter x et y sé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 devrait être mis en œuvre. La meilleure façon de le faire serait de le faire IMMUABLE.

Maintenant, par opposition à 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, étant donné que l'état n'est pas lié l'un à l'autre.

Le processus consistant à utiliser différents verrous pour protéger des états non liés est appelé Verrouillage de segmentation ou fractionnement de verrou

2
Narendra Pathai

Comme déjà dit ici, le bloc synchronisé peut utiliser une variable définie par l'utilisateur comme objet verrou, lorsque la fonction synchronisée utilise uniquement "this". 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 verrou. 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 à la variable locale la référence "this". Et comme résultat, nous aurons une taille de fonction un peu plus grande (non pertinente 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 obligé de le verrouiller; tous les autres voient que le verrou est pris et reviennent à l'état bloqué. Il ne s'agit pas simplement de gaspiller beaucoup de cycles de traitement: souvent, le changement de contexte pour débloquer un fil implique également la pagination de la mémoire à partir du disque, ce qui est extrêmement coûteux.

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

1
Roman

La raison pour laquelle vous ne synchronisez pas sur this 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 quand même besoin à l'état intermédiaire). Si vous verrouillez ceci, vous devez toujours vous rappeler lequel des deux verrous est ceci; 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 longOperation() sur deux instances différentes de BadObject, ils acquièrent leurs verrous; lorsqu'il est temps d'appeler l.onMyEvent(...), nous sommes bloqués 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.

1
18446744073709551615

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, alors 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;
    }
}
_
1
Michael

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 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 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
}
0
Kolarčík Václav

Évitez d'utiliser synchronized(this) comme mécanisme de verrouillage: cela verrouille toute l'instance de la classe 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. Synchronised peut être utilisé à l'intérieur du 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...
          }
        }
      }
    }
0
surendrapanday

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' est (ce qui est malheureusement ce que 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 d'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 plus de détails, 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 en 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 les choses trop faciles pour les programmeurs ne pas réfléchir sur quoi verrouiller, ce qui est une chose extrêmement importante à laquelle réfléchir si vous ne voulez pas rencontrer des problèmes dans un programme multi-threadé.

On ne peut pas dire que "généralement", vous ne voulez pas que les utilisateurs de votre classe soient capables de faire ce genre de choses ou que "généralement" vous voulez ... 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érons par exemple le printwriter qui utilise un verrou interne mais les utilisateurs ont du mal à l'utiliser à partir de plusieurs threads s'ils ne veulent pas que leur sortie s'entrelace.

Si votre verrou est accessible en dehors de la classe ou non, 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 éloigner 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) 'en utilisant un objet de verrouillage explicite et en l'exposant, mais je pense que c'est inutile si votre comportement est bien documenté et que vous savez réellement ce que verrouiller' cela 'signifie.

Note 2: Je ne suis pas d'accord avec l'argument voulant que si du code vole accidentellement votre verrou, 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.

0
Amit Mittal

Un bon exemple pour une 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 facilement de manière prolongée (é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, nous en dérivons, synchronized (ceci peut être la seule solution).

0
Bart Prokop

Cela dépend de la tâche que vous voulez faire, 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 y a aussi quelques Nice verrous dans l'API qui pourraient vous aider :)

0
Harald Schilly