Question

En Java, la manière idiomatique de déclarer des sections critiques dans le code est la suivante:

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

Presque tous les blocs se synchronisent sur ce , mais y a-t-il une raison particulière à cela? Y a-t-il d'autres possibilités? Existe-t-il des bonnes pratiques sur quel objet synchroniser? (comme des instances privées de Object ?)

Était-ce utile?

La solution

Tout d'abord, notez que les extraits de code suivants sont identiques.

public void foo() {
    synchronized (this) {
        // do something thread-safe
    }
}

et:

public synchronized void foo() {
    // do something thread-safe
}

faites exactement la même chose . Aucune préférence pour l'un ou l'autre, à l'exception de la lisibilité du code et du style.

Lorsque vous synchronisez des méthodes ou des blocs de code, il est important de savoir pourquoi vous faites une telle chose et quel objet vous verrouillez exactement, et pour < strong> quel but .

Notez également qu'il existe des situations dans lesquelles vous souhaiterez synchroniser côté client des blocs de code dans lesquels le moniteur que vous demandez (c'est-à-dire l'objet synchronisé) n'est pas nécessairement ceci. , comme dans cet exemple:

Vector v = getSomeGlobalVector();
synchronized (v) {
    // some thread-safe operation on the vector
}

Je vous suggère d’approfondir vos connaissances sur la programmation concurrente, cela vous sera très utile une fois que vous saurez exactement ce qui se passe dans les coulisses. Vous devriez consulter Programmation concurrente en Java , un excellent livre sur le sujet. Si vous souhaitez vous plonger rapidement dans le sujet, consultez Concurrence Java @ Sun

Autres conseils

Comme les intervenants précédents l'ont noté, il est recommandé de synchroniser un objet dont la portée est limitée (en d'autres termes, choisissez la portée la plus restrictive possible et utilisez-la.) En particulier, la synchronisation sur . Cette est une mauvaise idée, sauf si vous avez l'intention d'autoriser les utilisateurs de votre classe à obtenir le verrou.

Cependant, si vous choisissez de vous synchroniser sur un java.lang.String , un cas particulièrement déplorable se présente. Les chaînes peuvent être (et sont presque toujours en pratique) internées. Cela signifie que chaque chaîne de contenu égal - dans la ENTIRE JVM - s'avère être la même chaîne en coulisse. Cela signifie que si vous synchronisez sur n'importe quelle chaîne, une autre section de code (complètement disparate) qui se verrouille également sur une chaîne ayant le même contenu verrouille également votre code.

J'avais une fois résolu un blocage dans un système de production et suivi (très douloureusement) le blocage dans deux packages Open Source complètement disparates qui se synchronisaient chacun sur une instance de String dont le contenu était à la fois "LOCK" .

J'essaie d'éviter la synchronisation sur this car cela permettrait à toutes les personnes de l'extérieur possédant une référence à cet objet de bloquer ma synchronisation. Au lieu de cela, je crée un objet de synchronisation local:

public class Foo {
    private final Object syncObject = new Object();
    …
}

Je peux maintenant utiliser cet objet pour la synchronisation sans craindre que quiconque «vole» le verrou.

Juste pour souligner le fait qu'il existe également des librairies ReadWriteLocks disponibles en Java, appelées java.util.concurrent.locks.ReadWriteLock.

Dans la plupart des cas, je distingue mes verrouillages de "lecture" et "de mise à jour". Si vous utilisez simplement un mot clé synchronisé, toutes les lectures dans la même méthode / le même bloc de code seront «mises en file d'attente». Un seul thread peut accéder au bloc à la fois.

Dans la plupart des cas, vous ne devez jamais vous inquiéter des problèmes de concurrence si vous faites simplement de la lecture. C’est lorsque vous écrivez que vous vous inquiétez des mises à jour simultanées (entraînant une perte de données), ou de la lecture lors d’une écriture (mises à jour partielles), qui doit vous préoccuper.

Par conséquent, un verrou en lecture / écriture me semble plus logique lors de la programmation multithread.

Vous voudrez vous synchroniser sur un objet pouvant servir de mutex. Si l'instance actuelle (la référence this ) convient (pas un Singleton, par exemple), vous pouvez l'utiliser, car en Java, tout objet peut servir de Mutex.

Dans d'autres cas, vous voudrez peut-être partager un mutex entre plusieurs classes, si des instances de ces classes peuvent avoir besoin d'accéder aux mêmes ressources.

Cela dépend beaucoup de l'environnement dans lequel vous travaillez et du type de système que vous construisez. Dans la plupart des applications Java EE que j'ai vues, la synchronisation n'est pas vraiment nécessaire ...

Personnellement, je pense que les réponses qui insistent sur le fait qu’il n’est jamais ou rarement correct de synchroniser sur ce sont erronées. Je pense que cela dépend de votre API. Si votre classe est une implémentation threadsafe et que vous la documentez, utilisez this . Si la synchronisation ne consiste pas à rendre chaque instance de la classe globalement threadsafe lors de l'appel de ses méthodes publiques, vous devez utiliser un objet interne privé. Les composants de bibliothèque réutilisables souvent entrent dans la catégorie précédente - vous devez bien réfléchir avant d'empêcher l'utilisateur d'envelopper votre API dans une synchronisation externe.

Dans le premier cas, l'utilisation de this permet à plusieurs méthodes d'être appelées de manière atomique. PrintWriter en est un exemple. Vous pouvez générer plusieurs lignes (par exemple, une trace de pile dans la console / le consignateur) et vous assurer qu’elles apparaissent ensemble. Dans ce cas, le fait de masquer l’objet de synchronisation en interne est une véritable gêne. Les wrappers de collection synchronisés sont un autre exemple. Dans ce cas, vous devez synchroniser sur l'objet de collection lui-même pour pouvoir effectuer une itération. Comme l'itération consiste en plusieurs appels de méthode, vous ne pouvez pas le protéger totalement en interne.

Dans ce dernier cas, j'utilise un objet simple:

private Object mutex=new Object();

Cependant, après avoir vu de nombreux dumps JVM et traces de pile indiquant qu'un verrou est "une instance de java.lang.Object ()" Je dois dire que l'utilisation d'une classe interne pourrait souvent être plus utile, comme d'autres l'ont suggéré.

Quoi qu'il en soit, cela vaut mes deux bits.

Éditer: Autre chose, lors de la synchronisation sur this , je préfère synchroniser les méthodes et conserver les méthodes très granulaires. Je pense que c'est plus clair et plus concis.

La synchronisation en Java implique souvent la synchronisation des opérations sur la même instance. La synchronisation sur ce est alors très idiomatique puisque ce est une référence partagée automatiquement disponible entre différentes méthodes d'instance (ou sections de) d'une classe.

Utiliser une autre référence spécifiquement pour le verrouillage, en déclarant et en initialisant un champ privé Object lock = new Object () , par exemple, est quelque chose dont je n'ai jamais eu besoin ni utilisé. Je pense que cela n’est utile que lorsque vous avez besoin d’une synchronisation externe sur deux ressources non synchronisées ou plus à l’intérieur d’un objet, bien que j’essaie toujours de reformuler une telle situation dans un format plus simple.

Quoi qu'il en soit, implicite (méthode synchronisée) ou explicite synchronisé (this) est beaucoup utilisé, également dans les bibliothèques Java. C'est un bon idiome et, le cas échéant, devrait toujours être votre premier choix.

La synchronisation dépend de ce que les autres threads susceptibles d'entrer en conflit avec cet appel de méthode peuvent synchroniser.

Si this est un objet utilisé par un seul thread et que nous accédons à un objet mutable qui est partagé entre les threads, un bon candidat est la synchronisation sur cet objet - synchronisation sur . ce n'a aucun intérêt, car un autre thread qui modifie cet objet partagé peut même ne pas connaître this , mais connaît cet objet.

D'autre part, la synchronisation sur , ce a du sens si plusieurs threads appellent les méthodes de cet objet en même temps, par exemple si nous sommes en singleton.

Notez qu'une méthode synchronisée n'est souvent pas la meilleure option, car nous gardons un verrou tout au long de son exécution. Si elle contient des parties consommant beaucoup de temps mais ne prenant pas le temps, et une partie ne prenant pas autant de temps avec les threads, la synchronisation sur la méthode est très fausse.

  

Presque tous les blocs se synchronisent sur ce point, mais y a-t-il une raison particulière à cela? Y a-t-il d'autres possibilités?

Cette déclaration synchronise la méthode entière.

private synchronized void doSomething() {

Cette déclaration a synchronisé une partie du bloc de code au lieu de la méthode entière.

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

De la documentation oracle page

rendre ces méthodes synchronisées a deux effets:

Tout d'abord, il n'est pas possible d'entrelacer deux appels de méthodes synchronisées sur le même objet . Lorsqu'un thread exécute une méthode synchronisée pour un objet, tous les autres threads qui invoquent des méthodes synchronisées pour le même bloc d'objet (suspendre l'exécution) jusqu'à la fin du premier thread avec l'objet.

  

Existe-t-il d'autres possibilités? Existe-t-il des meilleures pratiques sur quel objet sur lequel se synchroniser? (comme des instances privées d'Object?)

Il existe de nombreuses possibilités et alternatives à la synchronisation. Vous pouvez sécuriser votre thread de code en utilisant une concurrence de haut niveau API (disponible depuis la version 1.5 de JDK)

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

Reportez-vous aux questions SE ci-dessous pour plus de détails:

Synchronisation ou verrouillage

Éviter la synchronisation (ceci) en Java?

La meilleure pratique consiste à créer un objet uniquement pour fournir le verrou:

private final Object lock = new Object();

private void doSomething() {
  // thread-safe code
  synchronized(lock) {
    // thread-unsafe code
  }
  // thread-safe code
}

Vous êtes ainsi assuré qu'aucun code d'appel ne peut jamais bloquer votre méthode par une ligne non intentionnelle synchronisée (votreObjet) .

( Remerciements à @jared et @ yuval-adam qui l'ont expliqué plus en détail ci-dessus. )

Mon hypothèse est que l'utilisation de this dans les didacticiels est à l'origine du javadoc de Sun: https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

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