Synchronisation vs verrouillage

183

java.util.concurrentL'API fournit une classe appelée as Lock, qui sérialiserait essentiellement le contrôle afin d'accéder à la ressource critique. Il donne des méthodes telles que park()et unpark().

Nous pouvons faire des choses similaires si nous pouvons utiliser des synchronizedmots clés wait()et des notify() notifyAll()méthodes et des utilisations .

Je me demande lequel d'entre eux est le meilleur dans la pratique et pourquoi?

rêveur
la source
1
article utile ici: javarevisited.blogspot.in/2013/03/…
roottraveller

Réponses:

178

Si vous verrouillez simplement un objet, je préférerais utiliser synchronized

Exemple:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Vous devez le faire explicitement try{} finally{}partout.

Alors qu'avec synchronisé, c'est super clair et impossible de se tromper:

synchronized(myObject) {
    doSomethingNifty();
}

Cela dit, les Locks peuvent être plus utiles pour des choses plus compliquées où vous ne pouvez pas acquérir et publier de manière aussi propre. Je préférerais honnêtement éviter d'utiliser des bare Locks en premier lieu, et opter pour un contrôle de concurrence plus sophistiqué tel que a CyclicBarrierou a LinkedBlockingQueue, s'ils répondent à vos besoins.

Je n'ai jamais eu de raison d'utiliser wait()ou notify()mais il peut y en avoir de bonnes.

Steven Schlansker
la source
1
Quelle est la différence entre attendre / notifier et garer / débloquer LockSupport? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
Pacerier
6
Au début, l'exemple avait du sens avec les serrures, mais j'ai ensuite réalisé que si vous utilisez un bloc try finally, ce problème serait évité si les verrous ne sont pas libérés
William Reed
Ahh ... Un de ces moments pour apprécier le modèle RAII en C ++. std::lock_guard
WhiZTiM
67

Je me demande lequel d'entre eux est le meilleur dans la pratique et pourquoi?

J'ai trouvé que Locket Condition(et d'autres nouvelles concurrentclasses) ne sont que des outils supplémentaires pour la boîte à outils. Je pouvais faire presque tout ce dont j'avais besoin avec mon ancien marteau à griffes (le synchronizedmot - clé), mais il était difficile à utiliser dans certaines situations. Plusieurs de ces situations délicates sont devenues beaucoup plus simples une fois que j'ai ajouté plus d'outils à ma boîte à outils: un maillet en caoutchouc, un marteau à boule, un levier et des poinçons. Cependant , mon vieux marteau à griffes voit toujours sa part d'utilisation.

Je ne pense pas que l'un soit vraiment «meilleur» que l'autre, mais chacun convient mieux à différents problèmes. En un mot, le modèle simple et la nature orientée portée de synchronizedme protègent des bogues dans mon code, mais ces mêmes avantages sont parfois des obstacles dans des scénarios plus complexes. Ce sont ces scénarios plus complexes que le package simultané a été créé pour aider à résoudre. Mais l'utilisation de ces constructions de niveau supérieur nécessite une gestion plus explicite et prudente du code.

===

Je pense que JavaDoc fait un bon travail pour décrire la distinction entre Locket synchronized(l'accent est mis sur moi):

Les implémentations de verrouillage fournissent des opérations de verrouillage plus étendues que celles qui peuvent être obtenues à l'aide de méthodes et d'instructions synchronisées. Ils permettent une structuration plus flexible , peuvent avoir des propriétés très différentes et peuvent prendre en charge plusieurs objets Condition associés .

...

L'utilisation de méthodes ou d'instructions synchronisées permet d'accéder au verrou implicite du moniteur associé à chaque objet, mais oblige toutes les acquisitions et déblocages de verrous à se produire de manière structurée par blocs : lorsque plusieurs verrous sont acquis, ils doivent être libérés dans l'ordre inverse , et tous les verrous doivent être libérés dans la même portée lexicale dans laquelle ils ont été acquis .

Alors que le mécanisme de portée des méthodes et instructions synchronisées facilite la programmation avec les verrous de moniteur et permet d' éviter de nombreuses erreurs de programmation courantes impliquant des verrous, il est parfois nécessaire de travailler avec des verrous de manière plus flexible. Par exemple, * * certains algorithmes * pour parcourir des structures de données accédées simultanément nécessitent l'utilisation du "hand-over-hand" ou du "chain lock " : vous acquérez le verrou du nœud A, puis du nœud B, puis relâchez A et acquérez C, puis relâchez B et acquérez D et ainsi de suite. Les implémentations de l' interface de verrouillage permettent l'utilisation de telles techniques en permettant l'acquisition et la libération d'un verrou dans différentes portées , etpermettant l'acquisition et la libération de plusieurs verrous dans n'importe quel ordre .

Cette flexibilité accrue s'accompagne d'une responsabilité supplémentaire . L' absence de verrouillage structuré par blocs supprime la libération automatique des verrous qui se produit avec les méthodes et les instructions synchronisées. Dans la plupart des cas, l'idiome suivant doit être utilisé:

...

Lorsque le verrouillage et le déverrouillage se produisent dans différentes portées , il faut veiller à ce que tout le code exécuté pendant que le verrou est maintenu soit protégé par try-finally ou try-catch pour garantir que le verrou est libéré si nécessaire.

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 une tentative non bloquante d'acquérir un verrou (tryLock ()), une tentative d' acquérir le verrou qui peut être interrompu (lockInterruptiblement () et une tentative d' acquisition le verrou qui peut expirer (tryLock (long, TimeUnit)).

...

Bert F
la source
23

Vous pouvez obtenir tout ce que les services publics en java.util.concurrent font avec les primitives de bas niveau comme synchronized, volatileou attente / avisez

Cependant, la concurrence est délicate et la plupart des gens se trompent au moins en partie, ce qui rend leur code incorrect ou inefficace (ou les deux).

L'API simultanée fournit une approche de plus haut niveau, qui est plus facile (et en tant que telle plus sûre) à utiliser. En un mot, vous ne devriez plus avoir besoin d'utiliser synchronized, volatile, wait, notifydirectement.

La classe Lock elle-même se trouve sur le côté inférieur de cette boîte à outils, vous n'aurez peut-être même pas besoin de l'utiliser directement (vous pouvez utiliser Queueset Semaphore et d'autres choses, etc., la plupart du temps).

Thilo
la source
2
L'ancienne attente / notification est-elle considérée comme une primitive de niveau inférieur à celle du park / unpark de java.util.concurrent.locks.LockSupport, ou est-ce l'inverse?
Pacerier
@Pacerier: Je considère que les deux sont de bas niveau (c'est-à-dire quelque chose qu'un programmeur d'application voudrait éviter d'utiliser directement), mais certainement les parties de niveau inférieur de java.util.concurrency (telles que le package locks) sont construites par-dessus des primitives JVM natives wait / notify (qui sont encore de niveau inférieur).
Thilo
2
Non, je veux dire sur les 3: Thread.sleep / interruption, Object.wait / notify, LockSupport.park / unpark, quel est le plus primitif?
Pacerier
2
@Thilo Je ne sais pas comment vous soutenez votre déclaration qui java.util.concurrentest plus facile [en général] que les fonctionnalités du langage ( synchronized, etc ...). Lorsque vous utilisez, java.util.concurrentvous devez prendre l'habitude de terminer lock.lock(); try { ... } finally { lock.unlock() }avant d'écrire du code, alors qu'avec un, synchronizedvous êtes fondamentalement bien dès le début. Sur cette seule base, je dirais que synchronizedc'est plus facile (étant donné que vous voulez son comportement) que java.util.concurrent.locks.Lock. par 4
Iwan Aucamp
1
Ne pensez pas que vous pouvez répliquer exactement le comportement des classes AtomicXXX avec uniquement les primitives de concurrence, car elles reposent sur un appel CAS natif non disponible avant java.util.concurrent.
Duncan Armstrong
15

Il existe 4 facteurs principaux expliquant pourquoi vous voudriez utiliser synchronizedou java.util.concurrent.Lock.

Remarque: le verrouillage synchronisé est ce que je veux dire lorsque je parle de verrouillage intrinsèque.

  1. Lorsque Java 5 est sorti avec ReentrantLocks, ils se sont avérés avoir une différence de débit assez remarquable par rapport au verrouillage intrinsèque. Si vous recherchez un mécanisme de verrouillage plus rapide et que vous utilisez la version 1.5, pensez à jucReentrantLock. Le verrouillage intrinsèque de Java 6 est désormais comparable.

  2. jucLock a différents mécanismes de verrouillage. Verrouillage interruptible - essayez de verrouiller jusqu'à ce que le fil de verrouillage soit interrompu; verrouillage chronométré - essayez de verrouiller pendant un certain temps et abandonnez si vous ne réussissez pas; tryLock - tente de verrouiller, si un autre thread tient le verrou abandonner. Tout cela est inclus à part la simple serrure. Le verrouillage intrinsèque n'offre qu'un verrouillage simple

  3. Style. Si 1 et 2 ne tombent pas dans les catégories de ce qui vous préoccupe, la plupart des gens, moi y compris, trouveraient les séménatiques de verrouillage intrinsèque plus faciles à lire et moins verbeuses que le verrouillage jucLock.
  4. Conditions multiples. Un objet sur lequel vous vous verrouillez ne peut être notifié et attendu qu'un seul cas. La méthode newCondition de Lock permet à un seul verrou d'avoir plusieurs raisons d'attendre ou de signaler. Je n'ai pas encore besoin de cette fonctionnalité dans la pratique, mais c'est une fonctionnalité intéressante pour ceux qui en ont besoin.
John Vint
la source
J'ai aimé les détails de votre commentaire. J'ajouterais une puce supplémentaire - le ReadWriteLock fournit un comportement utile si vous avez affaire à plusieurs threads, dont seuls certains doivent écrire sur l'objet. Plusieurs threads peuvent lire simultanément l'objet et ne sont bloqués que si un autre thread y écrit déjà.
Sam Goldberg
5

Je voudrais ajouter quelques éléments en plus de la réponse de Bert F.

Locksprend en charge diverses méthodes pour un contrôle de verrouillage plus fin, qui sont plus expressifs que les moniteurs implicites ( synchronizedverrous)

Un verrou fournit un accès exclusif à une ressource partagée: un seul thread à la fois peut acquérir le verrou et tous les accès à la ressource partagée nécessitent que le verrou soit acquis en premier. Cependant, certains verrous peuvent autoriser l'accès simultané à une ressource partagée, comme le verrou en lecture d'un ReadWriteLock.

Avantages du verrouillage sur la synchronisation à partir de la page de documentation

  1. L'utilisation de méthodes ou d'instructions synchronisées permet d'accéder au verrou implicite du moniteur associé à chaque objet, mais oblige toutes les acquisitions et libérations de verrous à se produire de manière structurée par 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 une tentative non bloquante d'acquérir un lock (tryLock()), une tentative d'acquérir le verrou qui peut être interrompu ( lockInterruptibly()et une tentative d'acquérir le verrou qui peut timeout (tryLock(long, TimeUnit)).

  3. Une classe Lock peut également fournir un comportement et une sémantique assez différents de ceux du verrouillage implicite du moniteur, comme l' ordre garanti, l'utilisation non réentrante ou la détection de blocage.

ReentrantLock : En termes simples, selon ma compréhension, ReentrantLockpermet à un objet de rentrer d'une section critique à une autre section critique. Puisque vous avez déjà verrouillé pour entrer dans une section critique, vous pouvez une autre section critique sur le même objet en utilisant le verrou actuel.

ReentrantLockcaractéristiques clés selon cet article

  1. Possibilité de verrouiller de manière interruptible.
  2. Possibilité de temporiser en attendant le verrouillage.
  3. Le pouvoir de créer un verrouillage équitable.
  4. API pour obtenir la liste des threads en attente de verrouillage.
  5. Flexibilité pour essayer de verrouiller sans bloquer.

Vous pouvez utiliser ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLockpour acquérir davantage de contrôle sur le verrouillage granulaire sur les opérations de lecture et d'écriture.

En dehors de ces trois ReentrantLocks, java 8 fournit un autre Lock

StampedLock:

Java 8 est livré avec un nouveau type de verrou appelé StampedLock qui prend également en charge les verrous de lecture et d'écriture, comme dans l'exemple ci-dessus. Contrairement à ReadWriteLock, les méthodes de verrouillage d'un StampedLock renvoient un tampon représenté par une valeur longue.

Vous pouvez utiliser ces tampons pour libérer un verrou ou pour vérifier si le verrou est toujours valide. De plus, les verrous estampés prennent en charge un autre mode de verrouillage appelé verrouillage optimiste.

Jetez un œil à cet article sur l'utilisation de différents types de verrous ReentrantLocket StampedLock.

Ravindra babu
la source
4

La principale différence est l'équité, en d'autres termes, les demandes sont-elles traitées FIFO ou peut-il y avoir des barges? La synchronisation au niveau de la méthode garantit une allocation équitable ou FIFO du verrou. En utilisant

synchronized(foo) {
}

ou

lock.acquire(); .....lock.release();

n'assure pas l'équité.

Si vous avez beaucoup de conflits pour le verrou, vous pouvez facilement rencontrer des barges où les demandes plus récentes obtiennent le verrou et les demandes plus anciennes restent bloquées. J'ai vu des cas où 200 threads arrivent en peu de temps pour un verrou et le deuxième à arriver a été traité en dernier. C'est correct pour certaines applications, mais pour d'autres, c'est mortel.

Voir le livre "Java Concurrency In Practice" de Brian Goetz, section 13.3 pour une discussion complète de ce sujet.

Brian Tarbox
la source
5
"La synchronisation au niveau de la méthode garantit une allocation équitable ou FIFO du verrou." => Vraiment? Êtes-vous en train de dire qu'une méthode synchronisée se comporte différemment en termes d'équité que d'envelopper le contenu des méthodes dans un bloc {} synchronisé? Je ne le pense pas, ou ai-je mal compris cette phrase ...?
weiresr
Oui, bien que surprenant et contre-intuitif, c'est correct. Le livre de Goetz est la meilleure explication.
Brian Tarbox
Si vous regardez le code fourni par @BrianTarbox, le bloc synchronisé utilise un objet autre que "this" pour verrouiller. En théorie, il n'y a pas de différence entre une méthode synchronisée et le fait de placer le corps entier de ladite méthode dans un bloc synchronisé, tant que le bloc utilise "this" comme verrou.
xburgos
La réponse doit être modifiée pour inclure une citation et indiquer clairement que «assurance» est «garantie statistique» et non déterministe.
Nathan Hughes
Désolé, je viens de découvrir que j'ai par erreur voté contre cette réponse il y a quelques jours (clic maladroit). Malheureusement, SO actuellement ne me laisse pas revenir. J'espère que je pourrai le réparer plus tard.
Mikko Östlund
3

Le livre "Java Concurrency In Practice" de Brian Goetz, section 13.3: "... Comme le ReentrantLock par défaut, le verrouillage intrinsèque n'offre aucune garantie d'équité déterministe, mais les garanties d'équité statistique de la plupart des implémentations de verrouillage sont assez bonnes pour presque toutes les situations ..."

Bodrin
la source
2

Lock facilite la vie des programmeurs. Voici quelques situations qui peuvent être réalisées facilement avec la serrure.

  1. Verrouillez une méthode et libérez le verrou dans une autre méthode.
  2. Si vous avez deux threads travaillant sur deux morceaux de code différents, cependant, dans le premier thread a un pré-requis sur un certain morceau de code dans le deuxième thread (tandis que d'autres threads travaillent également sur le même morceau de code dans le second fil simultanément). Un verrou partagé peut résoudre ce problème assez facilement.
  3. Mettre en place des moniteurs. Par exemple, une simple file d'attente où les méthodes put et get sont exécutées à partir de nombreux autres threads. Cependant, vous ne voulez pas que plusieurs méthodes put (ou get) s'exécutent simultanément, ni les méthodes put et get s'exécutant simultanément. Une serrure privée vous facilite la vie pour y parvenir.

Alors que, le verrou et les conditions reposent sur le mécanisme synchronisé. Par conséquent, peut certainement être en mesure d'obtenir les mêmes fonctionnalités que vous pouvez obtenir en utilisant le verrou. Cependant, la résolution de scénarios complexes avec synchronisé peut vous rendre la vie difficile et vous empêcher de résoudre le problème réel.

Md Monjur Ul Hasan
la source
1

Différence majeure entre verrou et synchronisé:

  • avec les serrures, vous pouvez libérer et acquérir les serrures dans n'importe quel ordre.
  • avec synchronisé, vous ne pouvez déverrouiller les verrous que dans l'ordre dans lequel ils ont été acquis.
NKumar
la source
0

Verrouiller et synchroniser le bloc ont tous deux le même objectif, mais cela dépend de l'utilisation. Considérez la partie ci-dessous

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

Dans le cas ci-dessus, si un thread entre dans le bloc de synchronisation, l'autre bloc est également verrouillé. S'il existe plusieurs blocs de synchronisation de ce type sur le même objet, tous les blocs sont verrouillés. Dans de telles situations, java.util.concurrent.Lock peut être utilisé pour empêcher le verrouillage indésirable des blocs

Shyam
la source