Avant Java 5.0, les seuls mécanismes qui pourraient être utilisés pour coordonner l'accès aux objets partagés ont été synchronisés et volatils. Nous savons que le mot-clé synchronisé implémente les verrous intégrés, tandis que le mot-clé volatil garantit une visibilité de la mémoire pour les multiples. Dans la plupart des cas, ces mécanismes peuvent bien faire le travail, mais ils ne peuvent pas mettre en œuvre des fonctions plus avancées, telles qu'elles ne sont pas en mesure d'interrompre un fil en attente pour acquérir un verrou, étant incapable de mettre en œuvre un mécanisme de verrouillage limité dans le temps, ne pas être en mesure de mettre en œuvre des règles de verrouillage pour les structures non bloquantes, etc. et ces mécanismes de verrouillage plus flexibles offrent généralement une meilleure activité ou des performances. Par conséquent, un nouveau mécanisme a été ajouté dans Java 5.0: Reentrantlock. La classe reentrantlock implémente l'interface de verrouillage et fournit le même mutex et la même visibilité de la mémoire que synchronisée. Sa couche sous-jacente est d'atteindre la synchronisation multi-thread via AQS. Par rapport aux verrous intégrés, ReentrantLoc fournit non seulement un mécanisme de verrouillage plus riche, mais n'est pas non plus inférieur aux verrous intégrés en performances (encore mieux que les verrous intégrés dans les versions précédentes). Ayant parlé de tant d'avantages de Reentrantlock, découvrons son code source et voyons son implémentation spécifique.
1. Introduction aux mots clés synchronisés
Java fournit des verrous intégrés pour prendre en charge la synchronisation multi-thread. Le JVM identifie le bloc de code synchronisé en fonction du mot clé synchronisé. Lorsqu'un thread entre dans le bloc de code synchronisé, il acquiert automatiquement le verrou. Lors de la sortie du bloc de code synchronisé, le verrou est automatiquement libéré. Une fois qu'un thread a acquis le verrou, d'autres threads seront bloqués. Chaque objet Java peut être utilisé comme verrouillage qui met en œuvre la synchronisation. Le mot-clé synchronisé peut être utilisé pour modifier les méthodes d'objet, les méthodes statiques et les blocs de code. Lors de la modification des méthodes d'objet et des méthodes statiques, le verrou est l'objet où se trouve la méthode et l'objet de classe. Lors de la modification du bloc de code, les objets supplémentaires doivent être fournis sous forme de verrous. La raison pour laquelle chaque objet Java peut être utilisé comme verrouillage est qu'un objet de moniteur (manipulation) est associé dans l'en-tête de l'objet. Lorsque le thread entre dans le bloc de code synchrone, il maintiendra automatiquement l'objet Monitor, et lorsqu'il sortira, il libérera automatiquement l'objet Monitor. Lorsque l'objet Monitor est maintenu, d'autres threads seront bloqués. Bien sûr, ces opérations de synchronisation sont implémentées par la couche sous-jacente JVM, mais il existe encore quelques différences dans l'implémentation sous-jacente de la méthode de modification des mots clés synchronisée et du bloc de code. La méthode de modification des mots clés synchronisée est implicitement synchronisée, c'est-à-dire qu'elle n'a pas besoin d'être contrôlée via des instructions ByteCode. Le JVM peut distinguer si une méthode est une méthode synchronisée basée sur l'indicateur d'accès Acc_Synchronisé dans le tableau de la méthode; Alors que les blocs de code modifiés par mot clé synchronisé sont explicitement synchronisés, qui contrôlent la maintenance et la libération du thread du pipeline via les instructions de bytecode monitorexit et monitorexit. L'objet Monitor contient le champ _Count en interne. _Count égal à 0 signifie que le pipeline n'est pas maintenu, et _count supérieur à 0 signifie que le pipeline a été maintenu. Chaque fois que le thread de maintien rentre, _Count sera ajouté 1, et chaque fois que le thread de maintien sort, _Count sera réduit de 1. Il s'agit du principe d'implémentation de la réintégration de verrouillage intégrée. De plus, il y a deux files d'attente à l'intérieur de l'objet Monitor _entryList et _WaitSet, qui correspondent à la file d'attente de synchronisation et à la file d'attente conditionnelle des AQ. Lorsque le thread ne parvient pas à acquérir le verrou, il bloquera dans la _entrylist. Lorsque la méthode d'attente de l'objet de verrouillage est appelée, le thread entrera le _WAITSET pour attendre. Il s'agit du principe de mise en œuvre de la synchronisation du thread et de l'attente conditionnelle pour les verrous intégrés.
2. Comparaison entre reentrantlock et synchronisé
Le mot-clé synchronisé est un mécanisme de verrouillage intégré fourni par Java. Ses opérations de synchronisation sont mises en œuvre par le JVM sous-jacent. Reentrantlock est un verrou explicite fourni par le package java.util.concurrent, et ses opérations de synchronisation sont alimentées par le synchroniseur AQS. Reentrantlock fournit la même sémantique sur le verrouillage et la mémoire que les verrous intégrés, en plus il offre d'autres fonctionnalités, notamment l'attente de verrouillage chronométré, l'attente de verrouillage interruptible, le verrouillage équitable et la mise en œuvre de verrouillage structuré non block. De plus, Reentrantlock avait également certains avantages de performance dans les premières versions JDK. Étant donné que Reentrantlock a tant d'avantages, pourquoi devrions-nous utiliser le mot-clé synchronisé? En fait, de nombreuses personnes utilisent ReentrantLock pour remplacer l'opération de verrouillage des mots clés synchronisés. Cependant, les serrures intégrées ont toujours leurs propres avantages. Les verrous intégrés sont familiers à de nombreux développeurs et sont plus simples et compacts. Étant donné que les verrous explicites doivent être appelés manuellement déverrouiller dans le bloc final, il est relativement plus sûr d'utiliser des verrous intégrés. Dans le même temps, il est plus susceptible d'améliorer les performances des blocs synchronisés plutôt que de reentrant à l'avenir. Étant donné que Synchronized est une propriété intégrée du JVM, il peut effectuer certaines optimisations, telles que l'optimisation de l'élimination des verrous pour les objets de verrouillage enclos, éliminant la synchronisation des verrous intégrés en augmentant la granularité du verrou, et si ces fonctions sont implémentées via des verrous basés sur la bibliothèque de classe, il est peu invoque. Ainsi, lorsque certaines fonctionnalités avancées sont nécessaires, Reentrantlock doit être utilisé, notamment: les opérations d'acquisition de verrouillage intimement, pollllables et interruptibles, les files d'attente équitables et les verrous de structure non blocks. Sinon, synchronisé doit être utilisé en premier.
3. Opérations de l'acquisition et de la libération de verrous
Examinons d'abord l'exemple de code à l'aide de reentrantlock pour ajouter des verrous.
public void doSomething () {// La valeur par défaut est d'obtenir un verrouillage de verrouillage non-fair lock = new reentrantLock (); essayez {// verrouillage Lock.lock () avant l'exécution; // Exécuter l'opération ...} enfin {// le verrouillage Lock.unlock () libère enfin; }}Ce qui suit est l'API pour acquérir et libérer des verrous.
// le fonctionnement de l'obtention de verrouillage public void lock () {sync.lock ();} // le fonctionnement de la libération de verrouillage public void unlock () {sync.release (1);}Vous pouvez voir que les opérations de l'acquisition du verrou et de la libération du verrou sont déléguées à la méthode de verrouillage et à la méthode de libération de l'objet Sync respectivement.
classe publique ReentrantLock implémente Lock, java.io.serializable {private final sync sync; Résumé STATIC CLASS SYNC étend AbstractQueEuedSynchronizer {Abstract Void Lock (); } // Synchronizer qui implémente la classe finale statique non-FAIR STATIQUE NONFAIRSYNC étend Sync {Final Void Lock () {...}} // Synchronizer qui implémente la classe finale statique statique FairSync étend Sync {final void Lock () {...}}}}Chaque objet ReentrantLock détient une référence de Type Sync. Cette classe de synchronisation est une classe intérieure abstraite. Il hérite de l'abstractqueueEdSynchronizer. La méthode de verrouillage à l'intérieur est une méthode abstraite. La synchronisation de la variable des membres de Reentrantlock est attribuée à la valeur pendant la construction. Jetons un coup d'œil à ce que font les deux méthodes de constructeur de Reentrantlock?
// Le constructeur sans paramètre par défaut ReentrantLock () {sync = new nonfairSync ();} // Le constructeur paramétré ReentrantLock (Boolean Fair) {Sync = Fair? new FairSync (): New nonfairSync ();}L'appel du constructeur sans paramètre par défaut affectera l'instance non FairSync à Sync, et le verrou est un verrouillage non-fair pour le moment. Le constructeur de paramètres permet aux paramètres de spécifier s'ils pour attribuer une instance FairSync ou une instance non FAIRSYNC à synchroniser. NonfairSync et FairSync héritent tous deux de la classe de synchronisation et réécrit la méthode Lock (), il existe donc quelques différences entre les verrous équitables et les verrous non-fair sur la manière d'obtenir des verrous, dont nous parlerons ci-dessous. Jetons un coup d'œil au fonctionnement de la libération de la serrure. Chaque fois que vous appelez la méthode unlock (), vous exécutez simplement l'opération Sync.release (1). Cette opération appellera la méthode de version () de la classe AbstractQueueEdSynchronizer. Revoyons à nouveau.
// Libérez l'opération de verrouillage (mode exclusif) publique final booléen version (int arg) {// Tournez le verrouillage du mot de passe pour voir s'il peut déverrouiller si (tryrelease (arg)) {// Obtenez le nœud de nœud de tête h = tête; // Si le nœud de tête n'est pas vide et que l'état d'attente n'est pas égal à 0, réveillez le nœud successeur if (h! = Null && h.waitstatus! = 0) {// Réveillez le nœud successeur UnparkSuccessor (h); } return true; } return false;}Cette méthode de version est l'API pour libérer les opérations de verrouillage fournies par AQS. Il appelle d'abord la méthode TryRelease pour essayer d'acquérir le verrou. La méthode TryRelease est une méthode abstraite, et sa logique d'implémentation est dans la synchronisation des sous-classes.
// Essayez de libérer le verrou protégé Boolean TryRelease (INT Releases) {int c = getState () - Releases; // Si le thread tenant le verrou n'est pas le thread actuel, une exception sera lancée si (thread.currentThread ()! = GetExclusiveOwnerThread ()) {Throw New illégalMonitorStateException (); } boolean free = false; // Si l'état de synchronisation est 0, cela signifie que le verrou est libéré si (c == 0) {// définit l'indicateur du verrouillage libéré comme true free = true; // Définissez le thread occupé sur vide SetExclusiveOwnerThread (null); } setState (c); retour gratuitement;}Cette méthode TryRelease acquérira d'abord de l'état de synchronisation actuel, soustria l'état de synchronisation actuel des paramètres passés à l'état du nouvel synchronisation, puis déterminez si le nouvel état de synchronisation est égal à 0. S'il est égal à 0, cela signifie que le verrouillage actuel est libéré. Définissez ensuite l'état de libération du verrou sur true, puis effacez le thread qui occupe actuellement le verrou et appelez enfin la méthode SetState pour définir le nouvel état de synchronisation et renvoyer l'état de libération du verrou.
4. Lock juste et verrouillage injuste
Nous savons quelle instance spécifique est le reentrantlock pointant vers la base de la synchronisation. Pendant la construction, la synchronisation de la variable membre sera attribuée. Si la valeur est attribuée à l'instance non-FairSync, cela signifie qu'il s'agit d'un verrouillage non-FAIR, et si la valeur est attribuée à l'instance FairSync, cela signifie qu'il s'agit d'un verrouillage équitable. S'il s'agit d'un verrouillage équitable, les threads obtiendront la serrure dans l'ordre dans lequel ils font les demandes, mais sur le verrou injuste, le comportement de coupure est autorisé: Lorsqu'un fil demande un verrou injuste, si l'état de la serrure devient disponible en même temps que la demande est émise, le thread sautera tous les filetages en attente dans la file d'attente pour obtenir le verrouillage directement. Jetons un coup d'œil à l'obtention de verrous déloyaux.
// Synchronizer injuste Synchronizer la classe finale statique NonfairSync étend Sync {// implémenter la méthode abstraite de la classe parent pour acquérir le verrouillage final de verrouillage () {// Utiliser la méthode CAS pour définir l'état de synchronisation si (comparaison de la vision (0, 1)) {// si le réglage est réussi, cela signifie que la location n'est pas occupante SetExclusiveOnDenerThread (ThreadRentHread;); } else {// Sinon, cela signifie que la serrure a été occupée, appelez acquérir et laissez la file de thread pour synchroniser la file d'attente pour obtenir acquérir (1); }} // La méthode pour essayer d'acquérir la serrure finale Boolean TryAcquire (int acquiert) {return nonfairtryacquire (acquérir); }} // acquérir des verrous en mode non-interruption (mode exclusif) public final void acquire (int arg) {if (! Tryacquire (arg) && acquirequeUeUed (addWaitit (node.exclusive), arg)) {selfinterrupt (); }}On peut voir que dans la méthode de verrouillage du verrouillage déloyal, le thread modifiera la valeur de l'état de synchronisation de 0 à 1 à la première étape du CAS. En fait, cette opération équivaut à essayer d'acquérir le verrou. Si le changement réussit, cela signifie que le thread a acquis le verrou, et il n'est plus nécessaire de faire la queue dans la file d'attente de synchronisation. Si le changement échoue, cela signifie que le verrouillage n'a pas été libéré lorsque le thread est venu pour la première fois, donc la méthode d'acquisition est appelée Next. Nous savons que cette méthode acquise est héritée de la méthode AbstractqueuedSynchronizer. Prenons cette méthode. Une fois que le thread est entré dans la méthode Acquire, le premier appelez la méthode Tryacquire pour essayer d'acquérir le verrou. Étant donné que non-FairSync écrase la méthode TryAcquire et appelle la méthode non fairtryacquire de la synchronisation de la classe parent dans la méthode, la méthode non fairtryacquire sera appelée ici pour essayer d'acquérir le verrou. Voyons ce que fait cette méthode spécifiquement.
// Acquisition déloyale de Lock Final Boolean NonfairTryAcquire (int acquiert) {// Obtenez le thread final actuel Current Final Current = Thread.currentThread (); // Obtenez l'état de synchronisation actuel int c = getState (); // Si l'état de synchronisation est 0, cela signifie que le verrou n'est pas occupé si (c == 0) {// utilise CAS pour mettre à jour l'état de synchronisation if (comparabledSetState (0, acquérir)) {// Définit le thread occupant actuellement le verrouillage SetExclusiveownerThread (actuel); Retour Vrai; } // Sinon, il est déterminé si le verrou est le thread actuel} else if (current == getExclusiveOwnerThread ()) {// Si le verrou est maintenu par le thread actuel, modifiez directement l'état de synchronisation actuel int nextc = c + acquise; if (nextc <0) {lancer une nouvelle erreur ("le nombre de verrouillage maximum dépassé"); } setState (nextC); Retour Vrai; } // Si le verrou n'est pas le thread actuel, renvoyez l'indicateur de défaillance return false;}La méthode non fairtryacquire est une méthode de synchronisation. Nous pouvons voir qu'après que le thread est entré dans cette méthode, il acquiert d'abord l'état de synchronisation. Si l'état de synchronisation est 0, utilisez l'opération CAS pour modifier l'état de synchronisation. En fait, c'est à nouveau acquérir la serrure. Si l'état de synchronisation n'est pas 0, cela signifie que le verrou est occupé. À l'heure actuelle, nous déterminerons d'abord si le fil qui maintient le verrou est le fil actuel. Dans l'affirmative, l'état de synchronisation sera augmenté de 1. Sinon, le fonctionnement de l'essai d'acquérir le serrure échouera. Ainsi, la méthode AddWaitter sera appelée pour ajouter le thread à la file d'attente de synchronisation. Pour résumer, en mode verrouillage déloyal, un fil essaiera d'acquérir deux verrous avant d'entrer dans la file d'attente de synchronisation. Si l'acquisition est réussie, elle n'entrera pas sur la file d'attente de file d'attente de file d'attente de file d'attente de synchronisation, sinon il entrera la file d'attente de file d'attente de file d'attente de file d'attente de file d'attente de file d'attente de synchronisation. Ensuite, jetons un œil à la façon d'obtenir des écluses équitables.
// Synchronizer qui implémente Fair Lock Static Final Class FairSync étend Sync {// Implémentez la méthode abstraite de la classe parent pour acquérir Lock Final Void Lock () {// Appeler Acquérir Acquérir et laisser la file de thread pour synchroniser la file d'attente pour obtenir Acquire (1); } // Essayez d'acquérir le verrouillage Boolean Final TryAcquire (int acquier) {// Obtenez le thread final de thread actuel Current = Thread.currentThread (); // Obtenez l'état de synchronisation actuel int c = getState (); // Si l'état de synchronisation 0 signifie que la serrure n'est pas occupée si (c == 0) {// défendez s'il y a un nœud avant dans la file d'attente de synchronisation if (! HasqueUeUdPrecessesurs () && comparable (0, acquérir)) {// s'il n'y a pas de nœud avant et l'état de synchronisation est défini avec succès, cela signifie que le verrouillage est acquis setExclusiveOwnerThread (courant); Retour Vrai; } // Sinon, déterminez si le thread actuel maintient le verrouillage} else if (current == getExclusiveOwnerThread ()) {// Si le thread actuel maintient le verrou, modifie directement l'état de synchronisation int nextc = c + acquise; if (nextc <0) {lancer une nouvelle erreur ("le nombre de verrouillage maximum dépassé"); } setState (nextC); Retour Vrai; } // Si le thread actuel ne tient pas le verrou, l'acquisition échoue return false; }} Lors de l'appel de la méthode de verrouillage du verrouillage équitable, la méthode d'acquisition sera appelée directement. De même, la méthode Acquire appelle d'abord la méthode FairSync Rewrite Tryacquire pour essayer d'acquérir le verrou. Dans cette méthode, la valeur de l'état de synchronisation est d'abord obtenue. Si l'état de synchronisation est 0, cela signifie que le verrou est libéré pour le moment. La différence par rapport à la verrouillage déloyal est qu'elle appellera d'abord la méthode HasqueUedPrecesses pour vérifier si quelqu'un fait la queue dans la file d'attente de synchronisation. Si personne ne fait la queue, la valeur de l'état de synchronisation sera modifiée. Vous pouvez voir que le Fair Lock adopte une méthode de courtoisie ici au lieu d'acquérir la serrure immédiatement. À l'exception de cette étape différente de la serrure déloyale, les autres opérations sont les mêmes. Pour résumer, nous pouvons voir que Fair Lock ne vérifie la statut de la serrure une fois avant d'entrer dans la file d'attente de synchronisation. Même si vous constatez que le verrou est ouvert, vous ne l'acquérez pas immédiatement. Au lieu de cela, vous laissez les threads dans la file d'attente de synchronisation l'acquérir en premier. Par conséquent, on peut s'assurer que l'ordre dans lequel tous les threads acquièrent les serrures sous la ferme serrure est d'abord et est arrivé, ce qui garantit également l'équité d'obtenir les serrures.
Alors pourquoi ne voulons-nous pas que toutes les serrures soient justes? Après tout, l'équité est un bon comportement et l'injustice est un mauvais comportement. Étant donné que les opérations de suspension et de réveil du thread ont des frais généraux importants, il affecte les performances du système, en particulier dans le cas d'une concurrence féroce, les verrous équitables entraîneront des opérations de suspension et de réveil fréquentes de threads, tandis que les verrous non-fair peuvent réduire de telles opérations, ils seront donc meilleurs que les verrous équitables en termes de performance. De plus, comme la plupart des threads utilisent des verrous pendant très peu de temps et que l'opération de réveil du thread aura un retard, il est possible que le thread B acquière le verrou et libère le verrou après l'avoir utilisé. Cela conduit à une situation gagnant-gagnant. Le moment où le thread A acquiert, le verrou n'est pas retardé, mais le thread B utilise le verrouillage à l'avance et son débit a également été amélioré.
5. Le mécanisme de mise en œuvre des files d'attente conditionnelles
Il y a des défauts dans la file d'attente des conditions intégrées. Chaque serrure intégrée ne peut avoir qu'une seule file d'attente de condition associée, ce qui fait que plusieurs threads attendent différents prédicats de condition sur la même file d'attente d'état. Ensuite, chaque fois que Notifyall est appelé, tous les fils d'attente seront éveillés. Lorsque le fil se réveille, il constate que ce n'est pas le prédicat de condition qu'il attend et qu'il sera suspendu. Cela conduit à de nombreuses opérations de réveil et de suspension inutiles, ce qui gaspillera beaucoup de ressources système et réduira les performances du système. Si vous souhaitez écrire un objet simultané avec plusieurs prédicats conditionnels, ou si vous souhaitez prendre plus de contrôle que la visibilité de la file d'attente conditionnelle, vous devez utiliser un verrou et une condition explicites au lieu de verrous intégrés et de files d'attente conditionnelles. Une condition et un verrou sont associés ensemble, tout comme une file d'attente de conditions et un verrou intégré. Pour créer une condition, vous pouvez appeler la méthode Lock.NewCondition sur le verrou associé. Regardons d'abord un exemple en utilisant la condition.
classe publique BoundAdBuffer {Final Lock Lock = new ReentrantLock (); condition finale notlull = lock.newCondition (); // Prédicat de condition: Notfull Final Condition Notempty = Lock.NewCondition (); // Prédicat de condition: objet final de Notorempty [] items = nouvel objet [100]; int putptr, Takeptr, count; // Méthode de production public void put (objet x) lève InterruptedException {lock.lock (); essayez {while (count == items.length) notull.await (); // La file d'attente est pleine et le fil attend des articles [putptr] dans la file d'attente notable. éléments [putptr] = x; if (++ putptr == items.length) putptr = 0; ++ count; NotEmpty.signal (); // La production est réussie, réveillez le nœud de la file d'attente NotEmpty} enfin {lock.unlock (); }} // Consommer Method Public Object Take () lève InterruptedException {lock.lock (); essayez {while (count == 0) notEmpty.Await (); // La file d'attente est vide, Thread attend l'objet x = éléments [TakePtr] dans la file d'attente NotEmpty; if (++ TakePtr == items.length) TakePtr = 0; --compter; notull.signal (); // La consommation est réussie, réveillez le nœud du retour de file d'attente notable x; } enfin {lock.unlock (); }}}Un objet de verrouillage peut générer plusieurs files d'attente de conditions, et deux files d'attente de conditions sont générées ici notables et notes. Lorsque le conteneur est plein, le fil qui appelle la méthode de put doit être bloqué. Attendez que le prédicat de condition soit vrai (le conteneur n'est pas satisfait) se réveille et continue de s'exécuter; Lorsque le conteneur est vide, le thread qui appelle la méthode de prise doit être bloqué. Attendez que le prédicat de condition soit vrai (le conteneur n'est pas vide) se réveille et continue de s'exécuter. Ces deux types de threads attendent en fonction des différents prédicats de condition, ils entrent donc dans deux files d'attente de conditions différentes pour bloquer et attendront le bon moment avant de réveiller en appelant l'API sur l'objet de condition. Ce qui suit est le code d'implémentation de la méthode NewCondition.
// Créer une condition publique de file d'attente de condition newCondition () {return sync.NewCondition ();} Résumé Static Class Sync étend AbstractQueuedSynchronizer {// Créer un nouvel objet de condition Final ConditionObject NewCondition () {return new conditionObject (); }}La mise en œuvre de la file d'attente de conditions sur ReentrantLock est basée sur AbstractqueEuedynchronizer. L'objet de condition que nous obtenons lors de l'appel de la méthode NewCondition est une instance de la classe interne ConditionObject d'Aqs. Toutes les opérations sur les files d'attente de conditions sont effectuées en appelant l'API fournie par ConditionObject. Pour une implémentation spécifique de ConditionObject, vous pouvez consulter mon article "Java Concurrency Series [4] ----- AbstractQueEuedSynchronizer Code Analysis Analysis Conditional Fitre" et je ne le répéterai pas ici. À ce stade, notre analyse du code source de Reentrantlock a pris fin. J'espère que la lecture de cet article aidera les lecteurs à comprendre et à maîtriser Reentrantlock.
Ce qui précède est tout le contenu de cet article. J'espère que cela sera utile à l'apprentissage de tous et j'espère que tout le monde soutiendra davantage Wulin.com.