Pourquoi utiliser un ReentrantLock si on peut utiliser synchronisé (ceci)?

317

J'essaie de comprendre ce qui rend le verrouillage de la concurrence si important si l'on peut l'utiliser synchronized (this). Dans le code factice ci-dessous, je peux faire soit:

  1. synchronisé la méthode entière ou synchroniser la zone vulnérable ( synchronized(this){...})
  2. OU verrouillez la zone de code vulnérable avec un ReentrantLock.

Code:

    private final ReentrantLock lock = new ReentrantLock(); 
    private static List<Integer> ints;

    public Integer getResult(String name) { 
        .
        .
        .
        lock.lock();
        try {
            if (ints.size()==3) {
                ints=null;
                return -9;
            }   

            for (int x=0; x<ints.size(); x++) {
                System.out.println("["+name+"] "+x+"/"+ints.size()+". values >>>>"+ints.get(x));
            }

        } finally {
            lock.unlock();
        } 
        return random;
}
adhg
la source
1
BTW tous les verrous intrinsèques java sont réentrants par nature.
Aniket Thakur
@pongapundit synchronized(this){synchronized(this){//some code}}ne causera donc pas de blocage. Pour le verrouillage intrinsèque s'ils obtiennent un moniteur sur une ressource et s'ils le veulent à nouveau, ils peuvent l'obtenir sans verrou mort.
Aniket Thakur

Réponses:

474

Un ReentrantLock n'est pas structuré , contrairement aux synchronizedconstructions - c'est-à-dire que vous n'avez pas besoin d'utiliser une structure de bloc pour le verrouillage et pouvez même maintenir un verrou entre les méthodes. Un exemple:

private ReentrantLock lock;

public void foo() {
  ...
  lock.lock();
  ...
}

public void bar() {
  ...
  lock.unlock();
  ...
}

Un tel flux est impossible à représenter via un seul moniteur dans une synchronizedconstruction.


En plus de cela, ReentrantLockprend en charge l' interrogation des verrous et les attentes de verrouillage interruptibles qui prennent en charge le délai d'attente . ReentrantLockprend également en charge la politique d' équité configurable , permettant une planification plus flexible des threads.

Le constructeur de cette classe accepte un paramètre d' équité facultatif . Lorsqu'ils sont définis true, en cas de conflit, les verrous favorisent l'octroi de l'accès au thread qui attend le plus longtemps. Sinon, ce verrou ne garantit aucun ordre d'accès particulier. Les programmes utilisant des verrous équitables accessibles par de nombreux threads peuvent afficher un débit global inférieur (c'est-à-dire qu'ils sont plus lents; souvent beaucoup plus lents) que ceux utilisant le paramètre par défaut, mais présentent des écarts de temps plus petits pour obtenir des verrous et garantir un manque de famine. Notez cependant que l'équité des verrous ne garantit pas l'équité de la programmation des threads. Ainsi, l'un des nombreux threads utilisant un verrou équitable peut l'obtenir plusieurs fois de suite tandis que d'autres threads actifs ne progressent pas et ne maintiennent pas actuellement le verrou. Notez également que letryLockne respecte pas le paramètre d'équité. Il réussira si le verrou est disponible même si d'autres threads sont en attente.


ReentrantLock peut également être plus évolutif , offrant de bien meilleures performances en cas de conflit élevé. Vous pouvez en savoir plus à ce sujet ici .

Cette allégation a cependant été contestée; voir le commentaire suivant:

Dans le test de verrouillage réentrant, un nouveau verrou est créé à chaque fois, il n'y a donc pas de verrouillage exclusif et les données résultantes ne sont pas valides. En outre, le lien IBM n'offre aucun code source pour le benchmark sous-jacent, il est donc impossible de déterminer si le test a même été effectué correctement.


Quand faut-il utiliser ReentrantLocks? Selon cet article de developerWorks ...

La réponse est assez simple - utilisez-la lorsque vous avez réellement besoin de quelque chose qu'elle ne fournit synchronizedpas, comme des attentes de verrouillage temporisées, des attentes de verrouillage interruptibles, des verrous non structurés, des variables de condition multiples ou une interrogation de verrouillage. ReentrantLockprésente également des avantages d'évolutivité, et vous devez l'utiliser si vous avez réellement une situation qui présente un conflit élevé, mais n'oubliez pas que la grande majorité des synchronizedblocs ne présentent pratiquement jamais de conflit, et encore moins un conflit élevé. Je conseillerais de développer avec la synchronisation jusqu'à ce que la synchronisation se révèle insuffisante, plutôt que de simplement supposer que "les performances seront meilleures" si vous utilisezReentrantLock. N'oubliez pas que ce sont des outils avancés pour les utilisateurs avancés. (Et les utilisateurs vraiment avancés ont tendance à préférer les outils les plus simples qu'ils peuvent trouver jusqu'à ce qu'ils soient convaincus que les outils simples sont inadéquats.) Comme toujours, faites-le bien d'abord, puis demandez-vous si vous devez ou non le rendre plus rapide.

oldrinb
la source
26
Le lien «connu pour être plus évolutif» vers lycog.com doit être supprimé. Dans le test de verrouillage réentrant, un nouveau verrouillage est créé à chaque fois, il n'y a donc pas de verrouillage exclusif et les données résultantes ne sont pas valides. De plus, le lien IBM n'offre aucun code source pour le benchmark sous-jacent, il est donc impossible de déterminer si le test a même été effectué correctement. Personnellement, je supprimerais simplement toute la ligne sur l'évolutivité, car la revendication entière n'est essentiellement pas prise en charge.
Dev
2
J'ai modifié le message à la lumière de votre réponse.
oldrinb
6
Si la performance vous préoccupe beaucoup, n'oubliez pas de chercher un moyen où vous n'avez besoin d'aucune synchronisation.
mcoolive
2
La performance n'a aucun sens pour moi. Si le verrou ré-intrant fonctionnerait mieux, pourquoi la synchronisation ne serait-elle pas simplement implémentée de la même manière qu'un verrou ré-intrant en interne?
tObi
2
@ user2761895 le ReentrantLockPseudoRandomcode dans le lien Lycog utilise de nouveaux verrous incontrôlés à chaque appel de setSeedandnext
oldrinb
14

ReentrantReadWriteLockest une serrure spécialisée alors qu'elle synchronized(this)est une serrure à usage général. Ils sont similaires mais pas tout à fait les mêmes.

Vous avez raison en ce que vous pourriez utiliser à la synchronized(this)place de ReentrantReadWriteLockmais l'inverse n'est pas toujours vrai.

Si vous souhaitez mieux comprendre ce qui rend la ReentrantReadWriteLockrecherche spéciale, des informations sur la synchronisation des threads producteur-consommateur.

En général, vous vous souvenez que la synchronisation de méthode entière et la synchronisation à usage général (en utilisant le synchronizedmot - clé) peuvent être utilisées dans la plupart des applications sans trop réfléchir à la sémantique de la synchronisation, mais si vous avez besoin d'extraire les performances de votre code, vous devrez peut-être explorer d'autres mécanismes de synchronisation plus fins ou à usage spécial.

Soit dit en passant, l'utilisation synchronized(this)- et en général le verrouillage à l'aide d'une instance de classe publique - peut être problématique car elle ouvre votre code à des verrous mortels potentiels parce que quelqu'un d'autre, sans le savoir, pourrait essayer de se verrouiller contre votre objet ailleurs dans le programme.

Mike Dinescu
la source
pour éviter les verrous public class MyLock { private final Object protectedLongLockingMonitor = new Object(); private long protectedLong = 0L; public void incrementProtectedLong() { synchronized(protectedLongLockingMonitor) { protectedLong++; } } }
mortels
9

Depuis la page de documentation d' Oracle sur ReentrantLock :

Un verrou d'exclusion mutuelle réentrant avec le même comportement de base et la même sémantique que le verrou de moniteur implicite accessible à l'aide de méthodes et d'instructions synchronisées, mais avec des capacités étendues.

  1. Un ReentrantLock appartient au dernier thread verrouillé avec succès, mais ne le déverrouille pas encore. Un thread invoquant un verrou reviendra, acquérant avec succès le verrou, lorsque le verrou n'appartient pas à un autre thread. La méthode retournera immédiatement si le thread actuel possède déjà le verrou.

  2. Le constructeur de cette classe accepte un paramètre d' équité facultatif . Lorsqu'ils sont définis sur true, en cas de conflit, les verrous favorisent l'octroi de l'accès au thread le plus long . Sinon, ce verrou ne garantit aucun ordre d'accès particulier.

Fonctionnalités clés de ReentrantLock selon cet article

  1. Possibilité de se verrouiller de façon interruptible.
  2. Possibilité d'expiration en attendant le verrouillage.
  3. Le pouvoir de créer une serrure é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.WriteLock pour acquérir davantage de contrôle sur le verrouillage granulaire des opérations de lecture et d'écriture.

Jetez un œil à cet article de Benjamen sur l'utilisation de différents types de ReentrantLocks

Ravindra babu
la source
2

Vous pouvez utiliser des verrous réentrants avec une politique d'équité ou un délai d'expiration pour éviter la famine des threads. Vous pouvez appliquer une politique d'équité des threads. cela aidera à éviter qu'un thread n'attende éternellement pour accéder à vos ressources.

private final ReentrantLock lock = new ReentrantLock(true);
//the param true turns on the fairness policy. 

La "politique d'équité" sélectionne le prochain thread exécutable à exécuter. Il est basé sur la priorité, le temps écoulé depuis la dernière exécution, bla bla

aussi, Synchronize peut bloquer indéfiniment s'il ne peut pas échapper au bloc. Reentrantlock peut avoir un délai d'expiration défini.

j2emanue
la source
1

Les verrous synchronisés n'offrent aucun mécanisme de file d'attente dans lequel, après l'exécution d'un thread, n'importe quel thread exécuté en parallèle peut acquérir le verrou. En raison de quoi le thread qui est là dans le système et qui fonctionne pendant une plus longue période de temps n'a jamais la chance d'accéder à la ressource partagée conduisant ainsi à la famine.

Les verrous réentrants sont très flexibles et ont une politique d'équité dans laquelle si un thread attend plus longtemps et après l'achèvement du thread en cours d'exécution, nous pouvons nous assurer que le thread en attente plus long a la possibilité d'accéder à la ressource partagée en diminuant ainsi le débit du système et en le rendant plus long.

Sumit Kapoor
la source
1

Supposons que ce code s'exécute dans un thread:

private static ReentrantLock lock = new ReentrantLock();

void accessResource() {
    lock.lock();
    if( checkSomeCondition() ) {
        accessResource();
    }
    lock.unlock();
}

Parce que le thread possède le verrou, il autorisera plusieurs appels à verrouiller (), donc il ré-entrera dans le verrou. Cela peut être réalisé avec un décompte de référence afin qu'il n'ait pas à acquérir à nouveau le verrouillage.

RonTLV
la source
0

Une chose à garder à l'esprit est:

le nom « ReentrantLock » donne un mauvais message sur les autres mécanismes de verrouillage qu'ils ne sont pas rentrants. Ce n'est pas vrai. Le verrou acquis via «synchronisé» est également ré-entrant en Java.

La principale différence est que «synchronisé» utilise un verrouillage intrinsèque (celui que possède chaque objet), contrairement à l'API de verrouillage.

Été
la source
0

Je pense que les méthodes wait / notify / notifyAll n'appartiennent pas à la classe Object car elles polluent tous les objets avec des méthodes rarement utilisées. Ils ont beaucoup plus de sens sur une classe Lock dédiée. Donc, de ce point de vue, il est peut-être préférable d'utiliser un outil qui est explicitement conçu pour le travail à accomplir - c'est-à-dire ReentrantLock.

Solubris
la source