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
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
é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
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
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!