Maison  >  Article  >  Java  >  Quels sont les différents verrous en Java ?

Quels sont les différents verrous en Java ?

PHPz
PHPzavant
2023-04-27 17:43:071607parcourir

Introduction à 15 types de verrous en Java

En lisant de nombreux articles sur la concurrence, divers verrous seront mentionnés, tels que les verrous équitables, les verrous optimistes, etc. Cet article présente la classification des différents verrous. Le contenu de l'introduction est le suivant :

Verrouillage équitable / verrouillage injuste

Serrure réentrante / serrure non réentrante

Serrure exclusive / serrure partagée

Verrouillage mutex / verrouillage en lecture-écriture

Verrouillage optimiste / verrouillage pessimiste

Serrure segmentée

Verrouillage de biais / verrouillage léger / verrouillage lourd

Verrouillage rotatif

Il existe de nombreux noms de serrure ci-dessus. Ces classifications ne font pas toutes référence à l'état de la serrure. Certaines font référence aux caractéristiques de la serrure, et d'autres font référence à la conception de la serrure. Le contenu résumé ci-dessous est une certaine explication de chaque serrure. nom.

Verrouillage équitable / verrouillage injuste

Verrouillage équitable

Un verrouillage équitable signifie que plusieurs threads acquièrent des verrous dans l'ordre dans lequel ils demandent des verrous.

Verrouillage injuste

Un verrouillage injuste signifie que l'ordre dans lequel plusieurs threads acquièrent des verrous n'est pas celui dans lequel ils demandent des verrous. Il est possible que le thread qui l'applique plus tard acquière le verrou avant le thread qui l'applique en premier. Il est possible que cela provoque une inversion des priorités ou une famine.

Pour Java ReentrantLock, vous spécifiez si le verrou est un verrou équitable via le constructeur et la valeur par défaut est un verrou injuste. L’avantage des verrous injustes est que le débit est supérieur à celui des verrous équitables. Pour Synchronized, c’est aussi un verrouillage injuste. Puisqu'il n'implémente pas la planification des threads via AQS comme ReentrantLock, il n'y a aucun moyen de le transformer en un verrou équitable.

Serrure réentrante / serrure non réentrante

Verrouillage réentrant

Un verrou réentrant au sens large fait référence à un verrou qui peut être appelé de manière répétée et récursive. Une fois le verrou utilisé dans la couche externe, il peut toujours être utilisé dans la couche interne sans interblocage (à condition qu'il s'agisse du même objet ou de la même classe). ). Un tel verrou est appelé un verrou réentrant. ReentrantLock et synchronisé sont tous deux des verrous réentrants

synchronisé void setA() lève une exception{

Discussion.sleep(1000);

​setB();

}

synchronisé void setB() lève une exception{

Discussion.sleep(1000);

}

Le code ci-dessus est une fonctionnalité d'un verrou réentrant, s'il ne s'agit pas d'un verrou réentrant, setB peut ne pas être exécuté par le thread actuel, ce qui peut provoquer un blocage.

Pas de verrouillage de rentrée

Les verrous non réentrants, contrairement aux verrous réentrants, ne peuvent pas être appelés de manière récursive et un blocage se produira si des appels récursifs sont effectués. J'ai vu une explication classique de l'utilisation d'un verrou tournant pour simuler un verrou non réentrant. Le code est le suivant

. importer java.util.concurrent.atomic.AtomicReference;

classe publique UnreentrantLock {

private AtomicReference propriétaire = new AtomicReference();

​public void lock() {

​Thread actuel = Thread.currentThread();

//Cette phrase est une syntaxe de "spin" très classique, que l'on retrouve également dans AtomicInteger

​pour (;;) {

​if (!owner.compareAndSet(null, current)) {

retour ;

}

}

}

​public void unlock() {

​Thread actuel = Thread.currentThread();

​owner.compareAndSet(current, null);

}

}

Le code est également relativement simple. Il utilise des références atomiques pour stocker les threads. Le même thread appelle la méthode lock() deux fois. Si unlock() n'est pas exécuté pour libérer le verrou, un blocage se produira lorsque le spin sera appelé pour la seconde fois. Ce verrou n'est pas réentrant, mais en fait, le même thread n'a pas besoin de libérer le verrou et de l'acquérir à chaque fois. Une telle commutation de planification consomme beaucoup de ressources.

​Transformez-le en serrure réentrante :

importer java.util.concurrent.atomic.AtomicReference;

classe publique UnreentrantLock {

private AtomicReference propriétaire = new AtomicReference();

état int privé = 0;

​public void lock() {

​Thread actuel = Thread.currentThread();

​if (actuel == propriétaire.get()) {

état++;

retour ;

}

//Cette phrase est une syntaxe de "spin" très classique, que l'on retrouve également dans AtomicInteger

​pour (;;) {

​if (!owner.compareAndSet(null, current)) {

retour ;

}

}

}

​public void unlock() {

​Thread actuel = Thread.currentThread();

​if (actuel == propriétaire.get()) {

if (indiquer != 0) {

état--;

} autre {

​owner.compareAndSet(current, null);

}

}

}

}

Avant d'exécuter chaque opération, déterminez si le détenteur du verrou actuel est l'objet actuel et utilisez le comptage d'état au lieu de libérer le verrou à chaque fois.

Implémentation du verrouillage réentrant dans ReentrantLock

Voici un aperçu de la méthode d’acquisition de verrous pour les verrous injustes :

final booléen nonfairTryAcquire (int acquiert) {

courant du fil final = Thread.currentThread();

​int c = getState();

​si (c == 0) {

 if (compareAndSetState (0, acquiert)) {

​setExclusiveOwnerThread(actuel);

renvoie vrai ;

}

}

//Ça y est

else if (current == getExclusiveOwnerThread()) {

​int nextc = c + acquiert;

​if (nextc < 0) // débordement

Lancer une nouvelle erreur ("Nombre maximum de verrous dépassé");

​setState(suivant);

renvoie vrai ;

}

retourner faux ;

}

Un état int volatile privé est maintenu dans AQS pour compter le nombre de réentrées et éviter les opérations fréquentes de maintien et de libération, ce qui non seulement améliore l'efficacité mais évite également les blocages.

Serrure exclusive / serrure partagée

Verrous exclusifs et verrous partagés Lorsque vous lisez ReeReentrantLock et ReentrantReadWriteLock sous le package C.U.T, vous constaterez que l'un d'eux est un verrou exclusif et l'autre est un verrou partagé.

Verrou exclusif : ce verrou ne peut être détenu que par un seul thread à la fois.

Verrou partagé : ce verrou peut être partagé par plusieurs threads. Un exemple typique est le verrou de lecture dans ReentrantReadWriteLock. Son verrou de lecture peut être partagé, mais son verrou d'écriture ne peut être exclusif qu'à la fois.

De plus, le partage des verrous de lecture peut garantir que la lecture simultanée est très efficace, mais la lecture et l'écriture, l'écriture et la lecture s'excluent mutuellement.

Les verrous exclusifs et partagés sont également implémentés via AQS, et différentes méthodes sont implémentées pour obtenir des verrous exclusifs ou partagés. Pour Synchronized, il s’agit bien sûr d’un verrou exclusif.

Verrouillage mutex / verrouillage en lecture-écriture

Verrouillage mutex

Verrouillez la ressource partagée avant d'y accéder et déverrouillez-la une fois l'accès terminé. Après le verrouillage, tout autre thread tentant de se verrouiller à nouveau sera bloqué jusqu'à ce que le processus en cours se déverrouille.

Si plusieurs threads sont bloqués lors du déverrouillage, tous les threads sur le verrou seront programmés dans l'état prêt. Le premier thread qui devient prêt effectuera l'opération de verrouillage et les autres threads attendront à nouveau. De cette façon, un seul thread peut accéder à la ressource protégée par le mutex

Verrouillage en lecture-écriture

Le verrou en lecture-écriture est à la fois un verrou mutex et un verrou partagé. Le mode de lecture est partagé et le mode d'écriture s'exclut mutuellement (verrouillage exclusif).

Il existe trois états de verrouillage en lecture-écriture : l'état verrouillé en lecture, l'état verrouillé en écriture et l'état déverrouillé

L'implémentation spécifique du verrouillage en lecture-écriture en Java est ReadWriteLock

Un seul thread peut détenir un verrou en lecture-écriture en mode écriture à la fois, mais plusieurs threads peuvent détenir un verrou en lecture-écriture en mode lecture en même temps. Un seul thread peut occuper le verrou d'état d'écriture, mais plusieurs threads peuvent occuper le verrou d'état de lecture en même temps, c'est pourquoi il peut atteindre une concurrence élevée. Lorsqu'il est sous un verrou d'état d'écriture, tout thread souhaitant obtenir le verrou sera bloqué jusqu'à ce que le verrou d'état d'écriture soit libéré. ​​S'il est sous un verrou d'état de lecture, les autres threads sont autorisés à obtenir son verrou d'état de lecture, mais le sont ; n'est pas autorisé à l'obtenir jusqu'à ce que le verrou d'état de lecture de tous les threads soit libéré ; afin d'empêcher les threads qui souhaitent tenter d'écrire des opérations d'obtenir le verrou de statut d'écriture, lorsque le verrou de lecture-écriture détecte qu'un thread le souhaite. pour obtenir le verrou d'état d'écriture, il bloquera tous les threads suivants qui souhaitent obtenir le verrou d'état de lecture. Par conséquent, les verrous en lecture-écriture conviennent parfaitement aux situations dans lesquelles il y a beaucoup plus d’opérations de lecture sur les ressources que d’opérations d’écriture.

Verrouillage optimiste / verrouillage pessimiste

Verrouillage pessimiste

Supposons toujours le pire des cas. Chaque fois que vous récupérez les données, vous pensez que d'autres les modifieront, vous les verrouillerez donc à chaque fois que vous obtiendrez les données. De cette façon, si d'autres souhaitent obtenir les données, ils le feront. bloquer jusqu'à ce qu'ils obtiennent le verrou (ressource partagée Elle n'est utilisée que par un thread à la fois, les autres threads sont bloqués et les ressources sont transférées vers d'autres threads après utilisation). De nombreux mécanismes de verrouillage de ce type sont utilisés dans les bases de données relationnelles traditionnelles, tels que les verrous de ligne, les verrous de table, les verrous de lecture, les verrous d'écriture, etc., qui sont tous verrouillés avant les opérations. Les verrous exclusifs tels que synchronisés et ReentrantLock en Java sont l'implémentation de l'idée de verrouillage pessimiste.

Verrouillage optimiste

Supposons toujours la meilleure situation. Chaque fois que vous allez chercher les données, vous pensez que d'autres ne les modifieront pas, donc elles ne seront pas verrouillées. Cependant, lors de la mise à jour, vous jugerez si d'autres ont mis à jour les données pendant cette période. peut utiliser le mécanisme de numéro de version et l'implémentation de l'algorithme CAS. Le verrouillage optimiste convient aux types d'applications à lectures multiples, ce qui peut améliorer le débit. Le mécanisme write_condition fourni par la base de données est en fait un verrou optimiste. En Java, la classe de variables atomiques sous le package java.util.concurrent.atomic est implémentée à l'aide de CAS, une méthode d'implémentation de verrouillage optimiste.

Serrure segmentée

Le verrou segmenté est en fait une conception de verrou, et non un verrou spécifique. Pour ConcurrentHashMap, sa mise en œuvre de la concurrence consiste à réaliser des opérations simultanées efficaces sous la forme de verrous segmentés.

Le mécanisme de verrouillage des classes de conteneurs simultanées est basé sur des verrous segmentés avec une granularité plus petite. Les verrous segmentés sont également l'un des moyens importants pour améliorer les performances des programmes multi-simultanés.

Dans les programmes concurrents, les opérations en série réduiront l'évolutivité et le changement de contexte réduira également les performances. Ces deux problèmes surviendront lorsqu'une concurrence se produira sur le verrou. Lors de l'utilisation d'un verrou exclusif pour protéger des ressources restreintes, il s'agit essentiellement d'une méthode série : un seul thread peut y accéder à la fois. La plus grande menace pour l’évolutivité réside donc dans les verrous exclusifs.

Nous disposons généralement de trois manières de réduire le degré de concurrence des verrous : 1. Réduire le temps de maintien du verrou. 2. Réduire la fréquence des demandes de verrouillage. 3. Utiliser des verrous exclusifs avec des mécanismes de coordination. Ces mécanismes permettent une concurrence plus élevée.

Dans certains cas, nous pouvons étendre davantage la technique de décomposition du verrou pour décomposer le verrou sur un ensemble d'objets indépendants, ce qui devient un verrou segmenté.

​En fait, pour faire simple :

Il y a plusieurs verrous dans le conteneur, et chaque verrou est utilisé pour verrouiller une partie des données dans le conteneur. Ensuite, lorsque plusieurs threads accèdent aux données dans différents segments de données dans le conteneur, il n'y aura pas de concurrence de verrouillage entre les threads, ce qui peut effectivement être le cas. Améliorer l'efficacité de l'accès simultané. , c'est la technologie de segmentation des verrous utilisée par ConcurrentHashMap. Tout d'abord, les données sont divisées en segments pour le stockage, puis un verrou est attribué à chaque segment de données lorsqu'un thread occupe le verrou pour y accéder. segment de données, les données des autres segments sont également accessibles par d'autres threads.

Par exemple : un tableau contenant 16 verrous est utilisé dans ConcurrentHashMap. Chaque verrou protège 1/16 de tous les compartiments de hachage, et le Nième compartiment de hachage est protégé par le (N mod 16)ème verrou. En supposant qu'un algorithme de hachage raisonnable soit utilisé pour répartir les clés de manière égale, cela peut réduire grossièrement les demandes de verrouillage à 1/16. C'est cette technologie qui permet à ConcurrentHashMap de prendre en charge jusqu'à 16 threads d'écriture simultanés.

Verrouillage de biais / verrouillage léger / verrouillage lourd

Statut de verrouillage :

Statut débloqué

État de verrouillage de biais

​Statut de verrouillage léger

Statut de verrouillage des poids lourds

L'état du verrouillage est indiqué par les champs dans l'en-tête de l'objet du moniteur d'objets. Les quatre États vont progressivement s'intensifier avec la concurrence, et il s'agit d'un processus irréversible, c'est-à-dire qu'il ne peut être dégradé. Ces quatre états ne sont pas des verrous dans le langage Java, mais des optimisations apportées par Jvm pour améliorer l'efficacité de l'acquisition et de la libération des verrous (lors d'une utilisation synchronisée).

Verrouillage des biais

Le verrouillage biaisé signifie qu'un morceau de code de synchronisation est toujours accédé par un thread, le thread acquerra alors automatiquement le verrou. Réduisez le coût d’acquisition des serrures.

​Léger

Un verrou léger signifie que lorsque le verrou est un verrou biaisé et qu'un autre thread y accède, le verrou biaisé sera mis à niveau vers un verrou léger. D'autres threads tenteront d'acquérir le verrou via spin, ce qui ne bloquera pas et n'améliorera pas les performances.

Serrure lourde

Un verrou lourd signifie que lorsque le verrou est un verrou léger, même si un autre thread tourne, la rotation ne continuera pas éternellement. Lorsqu'il tourne un certain nombre de fois et que le verrou n'a pas été acquis, il entrera en blocage, le verrou. se transforme en un verrou lourd. Les verrous lourds bloqueront d’autres threads d’application et réduiront les performances.

Verrouillage rotatif

Nous savons que l'algorithme CAS est une méthode d'implémentation de verrouillage optimiste. L'algorithme CAS implique également des verrous tournants, je vais donc vous dire ici ce qu'est un verrou tournant.

Un bref aperçu de l'algorithme CAS

CAS est le mot anglais Compare and Swap (Compare and Swap), qui est un célèbre algorithme sans verrouillage. La programmation sans verrouillage signifie synchroniser des variables entre plusieurs threads sans utiliser de verrous, c'est-à-dire synchroniser les variables sans que les threads soient bloqués, c'est pourquoi on l'appelle également synchronisation non bloquante. L'algorithme CAS implique trois opérandes

La valeur mémoire V

qui doit être lue et écrite La valeur à comparer A

La nouvelle valeur à écrire B

Lors de la mise à jour d'une variable, uniquement lorsque la valeur attendue A de la variable est la même que la valeur réelle dans l'adresse mémoire V, la valeur correspondant à l'adresse mémoire V sera modifiée en B, sinon aucune opération ne sera effectuée. Généralement, il s'agit d'une opération de rotation, c'est-à-dire de tentatives constantes.

Qu'est-ce qu'un verrou tournant ?

Spin lock (spinlock) : lorsqu'un thread acquiert un verrou, si le verrou a été acquis par un autre thread, le thread attendra en boucle, puis continuera à juger si le verrou peut être acquis avec succès jusqu'à ce que le verrou soit acquis. Sortez de la boucle.

Il s'agit d'un mécanisme de verrouillage proposé pour protéger les ressources partagées. En fait, les verrous tournants sont similaires aux verrous mutex. Ils sont tous deux conçus pour résoudre l’utilisation mutuellement exclusive d’une certaine ressource. Qu'il s'agisse d'un verrou mutex ou d'un verrou tournant, il ne peut y avoir qu'un seul détenteur à tout moment. En d'autres termes, au plus une unité d'exécution peut obtenir le verrou à tout moment. Mais les deux ont des mécanismes de planification légèrement différents. Pour les verrous mutex, si la ressource est déjà occupée, le demandeur de ressource ne peut entrer qu'en état de veille. Cependant, le verrou tournant ne fera pas dormir l'appelant. Si le verrou tournant a été détenu par une autre unité d'exécution, l'appelant continuera à y faire une boucle pour voir si le détenteur du verrou tournant a libéré le mot « spin ». C'est pourquoi il tire son nom.

Comment implémenter le verrouillage rotatif en Java ?

​Voici un exemple simple :

classe publique SpinLock {

private AtomicReference cas = new AtomicReference();

​public void lock() {

​Thread actuel = Thread.currentThread();

//Utilisez CAS

​while (!cas.compareAndSet(null, current)) {

// NE NE rien FAIRE

}

}

​public void unlock() {

​Thread actuel = Thread.currentThread();

​cas.compareAndSet(current, null);

}

}

La méthode lock() utilise CAS Lorsque le premier thread A acquiert le verrou, il peut l'acquérir avec succès et n'entrera pas dans la boucle while. Si le thread A ne libère pas le verrou à ce moment, un autre thread B acquerra à nouveau le verrou. À ce moment-là, comme le CAS n'est pas satisfait, il entrera dans une boucle while et jugera en continu si le CAS est satisfait jusqu'à ce que le thread A appelle la méthode de déverrouillage pour libérer le verrou.

Problèmes avec les verrous rotatifs

1. Si un thread détient le verrou pendant trop longtemps, les autres threads en attente d'acquisition du verrou entreront dans une boucle et consommeront du processeur. Une utilisation inappropriée peut entraîner une utilisation extrêmement élevée du processeur. 2. Le verrou tournant implémenté dans Java ci-dessus n'est pas équitable, c'est-à-dire qu'il ne peut pas satisfaire le thread avec le temps d'attente le plus long pour acquérir le verrou en premier. Des verrous injustes entraîneront des problèmes de « famine de threads ».

Avantages du verrouillage rotatif

1. Le verrouillage de rotation ne fera pas changer l'état du thread, et il sera toujours dans l'état utilisateur, c'est-à-dire que le thread sera toujours actif, il ne fera pas entrer le thread dans l'état de blocage, réduisant ainsi les changements de contexte inutiles ; , et la vitesse d'exécution sera rapide. 2. Sans rotation Lorsque le verrou ne peut pas être acquis, le verrou entrera dans l'état de blocage et entrera dans l'état du noyau. Lorsque le verrou est acquis, il doit être restauré à partir de l'état du noyau. ce qui nécessite un changement de contexte de thread. (Une fois le thread bloqué, il entre dans l'état de planification du noyau (Linux). Cela entraînera un basculement du système entre le mode utilisateur et le mode noyau, affectant sérieusement les performances du verrouillage)

Verrouillage rotatif réentrant et verrouillage rotatif non réentrant

Une analyse minutieuse du code au début de l'article montre qu'il ne prend pas en charge la réentrée, c'est-à-dire que lorsqu'un thread a acquis le verrou pour la première fois, il réacquiert le verrou avant qu'il ne soit libéré. avec succès la deuxième fois. Puisque CAS n'est pas satisfait, la deuxième acquisition entrera dans une boucle while et attendra. S'il s'agit d'un verrou réentrant, il devrait être acquis avec succès la deuxième fois.

De plus, même s'il peut être acquis avec succès une deuxième fois, lorsque le verrou est libéré pour la première fois, le verrou acquis pour la deuxième fois sera également libéré, ce qui est déraisonnable.

Afin d'implémenter un verrou réentrant, nous devons introduire un compteur pour enregistrer le nombre de threads qui acquièrent le verrou.

classe publique ReentrantSpinLock {

private AtomicReference cas = new AtomicReference();

nombre d'ints privés ;

​public void lock() {

​Thread actuel = Thread.currentThread();

​if (current == cas.get()) { // Si le thread actuel a acquis le verrou, augmentez le nombre de threads de un, puis retournez

compte++;

retour ;

}

​ // Si le verrou n'est pas acquis, passez par CAS

​while (!cas.compareAndSet(null, current)) {

// NE NE rien FAIRE

}

}

​public void unlock() {

​Thread cur = Thread.currentThread();

​if (cur == cas.get()) {

​if (count > 0) {//Si supérieur à 0, cela signifie que le thread actuel a acquis le verrou plusieurs fois, et la libération du verrou est simulée en décrémentant le nombre de un

Compter--;

} else {// Si count==0, le verrou peut être libéré, garantissant ainsi que le nombre de fois où le verrou est acquis est cohérent avec le nombre de fois où le verrou est libéré.

​cas.compareAndSet(cur, null);

}

}

}

}

Verrouillage rotatif et verrouillage mutex

Les verrous tournants et les verrous mutex sont des mécanismes destinés à protéger le partage des ressources.

Qu'il s'agisse d'un verrou tournant ou d'un verrou mutex, il ne peut y avoir qu'un seul support à la fois.

Si le thread qui acquiert le verrou mutex est déjà occupé, le thread entrera en état de veille ; le thread qui acquiert le verrou tournant ne dormira pas, mais attendra en boucle que le verrou soit libéré.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer