Galerie de cartes mentales Outil de verrouillage de concurrence Java
Il s'agit d'une carte mentale sur les outils de verrouillage de concurrence Java. Ces outils de verrouillage de concurrence peuvent jouer un rôle important dans la programmation simultanée de Java et vous aider à écrire du code simultané efficace et sûr.
Modifié à 2024-01-18 10:28:15Cent ans de solitude est le chef-d'œuvre de Gabriel Garcia Marquez. La lecture de ce livre commence par l'analyse des relations entre les personnages, qui se concentre sur la famille Buendía et raconte l'histoire de la prospérité et du déclin de la famille, de ses relations internes et de ses luttes politiques, de son métissage et de sa renaissance au cours d'une centaine d'années.
Cent ans de solitude est le chef-d'œuvre de Gabriel Garcia Marquez. La lecture de ce livre commence par l'analyse des relations entre les personnages, qui se concentre sur la famille Buendía et raconte l'histoire de la prospérité et du déclin de la famille, de ses relations internes et de ses luttes politiques, de son métissage et de sa renaissance au cours d'une centaine d'années.
La gestion de projet est le processus qui consiste à appliquer des connaissances, des compétences, des outils et des méthodologies spécialisés aux activités du projet afin que celui-ci puisse atteindre ou dépasser les exigences et les attentes fixées dans le cadre de ressources limitées. Ce diagramme fournit une vue d'ensemble des 8 composantes du processus de gestion de projet et peut être utilisé comme modèle générique.
Cent ans de solitude est le chef-d'œuvre de Gabriel Garcia Marquez. La lecture de ce livre commence par l'analyse des relations entre les personnages, qui se concentre sur la famille Buendía et raconte l'histoire de la prospérité et du déclin de la famille, de ses relations internes et de ses luttes politiques, de son métissage et de sa renaissance au cours d'une centaine d'années.
Cent ans de solitude est le chef-d'œuvre de Gabriel Garcia Marquez. La lecture de ce livre commence par l'analyse des relations entre les personnages, qui se concentre sur la famille Buendía et raconte l'histoire de la prospérité et du déclin de la famille, de ses relations internes et de ses luttes politiques, de son métissage et de sa renaissance au cours d'une centaine d'années.
La gestion de projet est le processus qui consiste à appliquer des connaissances, des compétences, des outils et des méthodologies spécialisés aux activités du projet afin que celui-ci puisse atteindre ou dépasser les exigences et les attentes fixées dans le cadre de ressources limitées. Ce diagramme fournit une vue d'ensemble des 8 composantes du processus de gestion de projet et peut être utilisé comme modèle générique.
Outil de verrouillage de concurrence Java
RésuméQueueSynchronizer
Introduction
Le cadre de base pour la création de verrous ou d'autres composants de synchronisation, principalement utilisé pour gérer l'état de synchronisation
L'utilisation principale est l'héritage
Le blocage ne peut se produire qu'à un moment donné, ce qui réduit le coût du changement de contexte.
hériter
RésuméPropriétaireSynchronizer
Implémentation de base d'un synchroniseur qui permet aux threads d'occuper le mode exclusif
Variables membres
Discussion transitoire privée exclusiveOwnerThread
Le thread occupant actuellement l'état de synchronisation
méthode principale
protected final void setExclusiveOwnerThread(Thread thread)
Définir un fil de discussion exclusif
Fil de discussion final protégé getExclusiveOwnerThread()
Obtenez le fil de discussion exclusif actuel
implémenter l'interface
Sérialisable
classe intérieure
Noeud de classe finale statique
Introduction
AQS utilise en interne une file d'attente de synchronisation FIFO bidirectionnelle pour terminer la gestion de l'état de synchronisation. Lorsqu'un thread ne parvient pas à obtenir, il est ajouté à la fin de la file d'attente. La structure de la file d'attente est Node ; dans AQS. La file d'attente de conditions utilise également la définition de Node, mais se trouve dans une autre file d'attente
Variables membres
constante statique
Nœud final statique PARTAGÉ = nouveau nœud ()
Indique que le nœud est en mode partagé
Nœud final statique EXCLUSIF = null
Indique que le nœud est en mode exclusif et qu'un seul thread détient l'état de synchronisation en même temps.
statique final int ANNULÉ = 1
Le nœud (thread) est dans cet état en raison d'un délai d'attente ou d'une interruption, et ne passera pas à d'autres états par la suite, et ne devrait plus être bloqué.
statique final int SIGNAL = -1
Lorsque le nœud (thread) est dans cet état, le nœud successeur est ou sera dans un état bloqué (via park), donc lorsque le nœud actuel libère le verrou ou est annulé, unpark doit être appelé pour réveiller le nœud successeur. .
statique final int CONDITION = -2
Indique que le nœud actuel (thread) est dans la file d'attente des conditions (via wait) et ne fait pas partie de la file d'attente de synchronisation jusqu'à ce que l'état soit mis à 0 à un certain moment (via un signal)
statique final int PROPAGATE = -3
Propager les verrous partagés (utilisés par le nœud principal)
volatile int état d'attente
En incluant les éléments CANCELLED/SIGNAL/CONDITION/PROPAGATE ci-dessus et 0 (indiquant qu'il n'est pas dans ces quatre états), un nombre non négatif indique que le nœud ne nécessite pas d'interaction
Aperçu du nœud volatile
Nœud précurseur
Nœud volatile suivant
nœud successeur
fil de discussion volatile
Le thread correspondant au nœud
Nœud suivantWaiter
Dans la file d'attente de synchronisation, il indique si le nœud est dans un état partagé ou exclusif, égal à SHARED pour indiquer le partage, et null pour indiquer exclusif dans la file d'attente conditionnelle, il indique une référence au nœud suivant ;
La condition ne peut être utilisée que lorsque la file d'attente de synchronisation est en mode exclusif, cette variable est donc conçue pour être partagée.
méthode principale
Méthode de construction
Nœud()
Utilisé pour établir des nœuds initiaux ou construire des nœuds PARTAGÉS
Nœud (thread thread, mode nœud)
À utiliser dans addWaiter
Nœud (thread thread, int waitStatus)
Utilisé en état
méthode membre
isShared() booléen final
return nextWaiter == PARTAGÉ
Prédécesseur final du nœud ()
Récupère le nœud prédécesseur, lève une exception lorsqu'il est nul
classe publique ConditionObject
Introduction
L’implémentation de l’interface Condition sert à l’implémentation de base de Lock. Utilisez Node pour construire une file d'attente. Chaque objet Condition contient une file d'attente FIFO (unidirectionnelle).
La file d'attente utilise la propriété nextWaiter de Node comme référence au nœud suivant.
implémenter l'interface
Condition
Sérialisable
Variables membres
constante statique
privé statique final int REINTERRUPT = 1
Indique la nécessité d'interrompre à nouveau en quittant l'attente
privé statique final int THROW_IE = -1
Indique qu'InterruptedException doit être levée en quittant l'attente
Nœud transitoire privé firstWaiter
Pointe vers le nœud principal de la file d'attente conditionnelle
Nœud transitoire privé lastWaiter
Pointe vers le nœud de queue de la file d'attente de conditions
méthode principale
public final void wait()
Faites entrer le thread appelant dans la file d'attente, libérez le verrou et entrez dans l'état de blocage. Une fois le nœud réveillé par signal/signalAll, essayez d'acquérir le verrou. Si le blocage est réveillé par une interruption, il répondra à l'interruption.
Nœud privé addConditionWaiter()
Ajoutez un nouveau nœud à la file d'attente.
Logique d'implémentation spécifique : lorsqu'il s'avère que lastWaiter est annulé, appelez unlinkCancelledWaiters pour effacer les méthodes annulées dans toute la file d'attente (firstWaiter et lastWaiter peuvent pointer vers de nouveaux nœuds, puis créez un nouveau nœud (état CONDITION) et ajoutez-le au ; fin de la file d'attente
privé vide unlinkCancelledWaiters()
Utilisez while pour effacer les nœuds dont waitStatus n'est pas CONDITION à partir de firstWaiter. Étant donné que l’appel de méthode a lieu avant que le verrou ne soit libéré, le processus en boucle n’a pas besoin d’être verrouillé. Afin d'éviter les résidus dans le GC, lorsqu'il n'y a pas de signal, cette méthode n'est utilisée qu'en cas d'expiration ou d'annulation.
Le waitStatus du nœud dans la file d'attente des conditions ne doit avoir que deux états : CONDITION et CANCELLED.
final int FullyRelease (nœud de nœud)
Libérez l'état de verrouillage. Si l'état de verrouillage ne parvient pas à être libéré, une IllegalMonitorStateException sera levée et le nœud sera défini sur l'état CANCELED. En cas de succès, la valeur de l'état avant la libération sera renvoyée.
libérer
La méthode de publication d'AQS est appelée ici et un nœud approprié est trouvé pour se réveiller.
Les paramètres utilisent l'attribut state d'AQS
booléen final isOnSyncQueue (nœud de nœud)
Déterminez si le nœud est dans la file d'attente de synchronisation (notez qu'il s'agit d'une file d'attente de synchronisation plutôt que d'une file d'attente conditionnelle) : lorsque waitStatus est CONDITION, cela signifie qu'il n'est pas dans la file d'attente de synchronisation si le pré ou le suivant du nœud est. null, cela signifie également qu'il n'est pas dans la file d'attente de synchronisation (ces deux attributs sont utilisés pour la file d'attente de synchronisation. Le prédécesseur et le successeur de la file d'attente conditionnelle n'utilisent pas ces deux attributs), puis appelez la méthode findNodeFromTail pour parcourir la synchronisation file d'attente du nœud de queue pour voir si le nœud s'y trouve.
La relation entre la file d'attente de synchronisation et la file d'attente de conditions : la compétition de verrouillage repose uniquement sur la file d'attente de synchronisation. La file d'attente de conditions enregistre uniquement les threads bloqués en raison du manque de conditions. Lorsque les threads doivent rivaliser pour les verrous, ils doivent toujours être convertis en file d'attente de synchronisation.
Cette méthode est utilisée comme condition de jugement de while dans le code. Si le nœud n'est pas dans la file d'attente de synchronisation (indiquant qu'il n'est pas signal/signalAll), il entrera dans le while et bloquera le stationnement. Lorsque le nœud est réveillé, il déterminera d'abord s'il a été réveillé par une interruption. Sinon, il continuera à revenir au processus while. Sinon, il essaiera d'ajouter le nœud à la file d'attente de synchronisation et de sortir du processus while. processus.
acquérirQueued (nœud, état enregistré)
Essayer constamment d'obtenir l'état de synchronisation savingState pour les nœuds de nœud dans la file d'attente de synchronisation
dissocierAnnuléWaiters
Après avoir acquis le verrou, si node.nextWaiter n'est pas nul, appelez cette méthode pour effacer le nœud dans l'état annulé.
reportInterruptAfterWait
Si elle a été réveillée en raison d'une interruption auparavant, l'interruption doit être traitée en fonction des résultats du jugement précédent, qu'il s'agisse de réinitialiser l'état de l'interruption ou de lever une exception d'interruption.
finale publique longue attenteNanos (long nanosTimeout)
La logique de base est similaire à wait, avec l'ajout d'un jugement de délai d'attente.
Je ne comprends pas très bien la dernière partie de transferAfterCancelledWait.
attente booléenne finale publique (longue durée, unité TimeUnit)
La logique de base est similaire à waitNanos, vous pouvez spécifier l'unité de temps et finalement elle est convertie en nanosecondes pour le calcul.
public final void waitUninterruptible()
La logique de base est similaire à wait et ne répond pas aux interruptions (c'est-à-dire que selfInterrupt réinitialise l'état de l'interruption)
public final booléen waitUntil (Date limite)
La logique de base est similaire à wait, avec en plus un temps de blocage maximum.
signal de vide final public()
Déplacer le thread en attente le plus long de la file d'attente conditionnelle vers la file d'attente de synchronisation
booléen protégé isHeldExclusively()
Renvoie si le thread actuel détient le verrou en mode exclusif, si c'est le cas, renvoie vrai. Cette méthode est en AQS et ne fournit pas d'implémentation spécifique. Cependant, comme cette méthode n'est utilisée que dans condition, elle n'a pas besoin d'être implémentée si ConditionObject n'est pas utilisé, sinon elle doit être implémentée.
private void doSignal (nœud en premier)
Supprimez les nœuds de la file d'attente de conditions et convertissez-les en nœuds de la file d'attente de synchronisation. Le processus d'implémentation est une boucle, d'avant en arrière, jusqu'à ce qu'il rencontre un nœud qui n'est pas nul et qui n'est pas à l'état ANNULÉ, le convertit et quitte la boucle.
transfert booléen finalForSignal (nœud de nœud)
Convertissez le nœud de la file d'attente conditionnelle en file d'attente de synchronisation et indiquez si la conversion a réussi. La logique de mise en œuvre consiste à déterminer d'abord si l'état du nœud est CONDITION. S'il ne s'agit pas d'un nœud annulé, retournez false ; puis appelez enq pour entrer dans la file d'attente de synchronisation et obtenir le nœud prédécesseur du nœud dans la file d'attente. l'état du nœud prédécesseur est > 0 ou CAS est modifié. Si waitStatus échoue (des modifications ont eu lieu), relancez le thread et retournez enfin true.
Il ne se réveille pas nécessairement directement après avoir rejoint la file d'attente de synchronisation. Ce n'est que lorsque waitStatus>0 ou change pendant l'exécution de CAS que le thread sera déparqué et réveillé. Si vous ne vous réveillez pas à ce moment-là, vous attendrez la logique de la file d'attente de synchronisation, puis vous vous réveillerez.
demande
public final void signalAll()
Transférez tous les nœuds qualifiés (non annulés) de la file d'attente de conditions vers la file d'attente de synchronisation. La logique de base est similaire à signal, la différence est que la méthode transferForSignal est exécutée en boucle
estHeldExclusivement
private void doSignalAll (nœud en premier)
transfertPourSignal
booléen final protégé hasWaiters()
Qu'il y ait des threads dans la file d'attente de conditions, la méthode d'implémentation consiste à parcourir la file d'attente de conditions depuis le début. S'il existe un nœud dont l'état est CONDITION, il renvoie vrai, sinon il renvoie faux.
Variables membres
variable statique
statique final long spinForTimeoutThreshold = 1000L ;
Le seuil utilisé pour le timeout. S'il est inférieur à cette valeur, il n'est pas nécessaire d'appeler park avec timeout, mais laissez la boucle continuer à s'exécuter. À ce moment, l'efficacité de la rotation est plus élevée.
privé statique final dangereux dangereux
Unsafe.getUnsafe(), les éléments suivants sont utilisés pour obtenir le décalage via la méthode objectFieldOffset, qui facilite les modifications ultérieures directement via l'opération CAS de Unsafe.
état long final statique privéOffset
État AQS
privé statique final long headOffset
Responsable AQS
longue queue finale statique privéeOffset
Queue AQS
privé statique final longue attenteStatusOffset
État d'attente du nœud
privé statique final long nextOffset
Nœud suivant
privé transitoire volatile Tête de nœud
Le nœud principal de la file d'attente de synchronisation, chargement différé, modifié uniquement via setHead lorsque le nœud principal existe, son statut ne peut pas être ANNULÉ ;
privé transitoire volatile Queue de nœud
Le nœud de queue de la file d'attente de synchronisation, chargement paresseux
état international volatil privé
Statut de synchronisation
méthode principale
protégé final int getState()
Renvoie la valeur actuelle de l'état de synchronisation
setState final void protégé (int newState)
Définir l'état de synchronisation actuel
protégé final booléen compareAndSetState (int attendu, int mise à jour)
CAS définit l'état et la couche inférieure appelle la méthode CAS de Unsafe.
Nœud privé addWaiter (mode nœud)
méthode de mise en file d'attente
Ajouter le thread actuel à la fin de la file d'attente en utilisant le mode spécifié
Il est à noter que
Le paramètre Node est utilisé pour spécifier le mode, et le thread est obtenu via currentThread
Dans l'implémentation, lorsque la queue n'est pas vide, essayez rapidement cas de définir la queue. En cas de succès, elle reviendra directement. En cas d'échec, la méthode enq sera appelée.
Nœud privé enq (nœud de nœud final)
Boucle (spin) CAS Lorsque tail est vide, CAS initialise head (nouveau nœud) en premier. Lorsque tail n'est pas vide, CAS définit tail.
Le nœud de paramètre ici est le nœud qui doit être ajouté plutôt que le mode.
public final booléen hasQueuedPredecessors()
Déterminez s'il y a un nœud avant le thread actuel, si c'est le cas, renvoyez vrai, sinon renvoyez faux. Utilisé pour la mise en place de serrures équitables. Implémentation spécifique : lorsque la file d'attente n'est pas vide, elle renvoie true s'il est jugé que le nœud successeur de head est vide ou qu'il ne s'agit pas du thread actuel.
Quand head.next est-il nul ?
public final booléen hasQueuedThreads()
return head != tail S'il y a des threads en attente dans la file d'attente;
public final booléen isQueued (fil de discussion)
Que le thread soit dans la file d'attente, l'implémentation consiste à boucler de la fin au début
public final int getQueueLength()
Obtenez la longueur de la file d'attente. La méthode d'implémentation consiste à boucler de la fin au début pour calculer le nombre de nœuds dont le thread n'est pas nul.
Collection finale publique<Thread> getQueuedThreads()
Obtient la collection de threads dans la file d’attente et la renvoie sous la forme de Collection. L'implémentation consiste à construire une ArrayList, à boucler de la fin au début pour ajouter des threads qui ne sont pas nuls, puis à renvoyer l'objet ArrayList.
propriété booléenne finale publique (condition ConditionObject)
Si l'objet de condition appartient au synchroniseur actuel AQS
mode exclusif
Un seul thread peut obtenir l'état de synchronisation en même temps
public final void acquérir (int arg)
Le mode exclusif obtient l'état de synchronisation et ignore les interruptions ; appelez d'abord tryAcquire pour tenter d'acquérir le verrou exclusif, et après un échec, appelez acquireQueued(addWaiter(Node.EXCLUSIVE), arg) pour essayer d'ajouter le thread à la file d'attente de synchronisation et déterminer le état du nœud, puis en fonction du résultat renvoyé (s'il y a (Interruption de thread) Appelez la méthode d'interruption de thread pour restaurer l'état d'interruption
l'acquisition elle-même ne répond pas aux interruptions, le traitement des interruptions consiste donc à restaurer l'état de l'interruption
booléen protégé tryAcquire (int arg)
Essayez d'obtenir un verrou exclusif, renvoyez vrai en cas de succès, faux en cas d'échec. L'implémentation spécifique est complétée par des sous-classes et la sécurité des threads doit être assurée lors de l'obtention de l'état de synchronisation.
final boolean acquireQueued (nœud final du nœud, argument int)
arg peut être compris comme une ressource qui doit être concurrencée. AQS est représenté en interne par l'état, mais en fait l'utilisation spécifique est définie par le développeur.
Essayez continuellement d'acquérir des verrous pour les threads déjà dans la file d'attente (le nœud a été créé par addWaiter)
Logique d'implémentation : faites tourner comme suit : si le nœud prédécesseur est head et acquiert le verrou avec succès, définissez le nœud actuel sur head et le retour n'est pas interrompu ; sinon, procédez aux opérations suivantes : appelez ShouldParkAfterFailedAcquire pour déterminer et définir l'état du nœud ; appelez parkAndCheckInterrupt pour bloquer le thread et vérifier l’état de l’interruption. Si une exception se produit pendant le processus, appelez CancelAcquire pour annuler l'acquisition.
En fait, même si le verrou n'est pas acquis, la boucle ne s'arrêtera pas. Lorsque l'état du nœud prédécesseur est défini avec succès et que le thread est interrompu avec park, le thread est suspendu ; Le nœud après head utilisera tryAcquire pour rivaliser avec le nouveau thread pour le verrou, indiquant que l'implémentation du verrou ici est injuste.
setHead vide privé (nœud de nœud)
Définissez le nœud sur head et définissez les attributs thread et prev sur null pour faciliter le GC. En fait, cela équivaut à sortir de la file d’attente en tête de la file d’attente.
booléen statique privé ShouldParkAfterFailedAcquire (Node pred, Node node)
Vérifiez et mettez à jour l'état du nœud prédécesseur du nœud qui n'a pas réussi à acquérir le verrou sur SIGNAL, et indiquez si le thread actuel doit être bloqué.
Logique d'implémentation : déterminez le waitStatus du nœud prédécesseur et renvoyez true lorsqu'il est SIGNAL ; lorsque >0, cela signifie que le nœud prédécesseur a été annulé et que le nœud prédécesseur sera ignoré dans une boucle jusqu'au waitStatus d'un nœud prédécesseur. <=0, puis renvoie false ; sinon CAS Définit le waitStatus du nœud prédécesseur sur SIGNAL et renvoie false.
parkAndCheckInterrupt() booléen final privé
Parcage d'appels, vérifiez l'état de l'interruption et revenez
return thread.interrupted() Le résultat renvoyé ici est utilisé pour déterminer si le thread a été réveillé en raison d'une interruption, s'il a été réveillé en raison d'une interruption, il sera toujours dans la boucle dans acquireQueued et sera à nouveau bloqué par park ( parce que la méthode interrompue() effacera l'indicateur d'interruption, afin que le stationnement puisse reprendre effet) lorsqu'il est réveillé par un stationnement, le verrou sera acquis lors du pseudo-réveil, il suffit de vérifier à nouveau via la boucle externe ;
Qu’est-ce qui cause une fausse excitation ?
private void CancelAcquire (nœud de nœud)
Appelé lorsque la méthode entière lève une exception, videz le thread du nœud, définissez le statut sur ANNULÉ et recherchez le nœud avec waitStatus<=0 comme précurseur, retirez-vous de la file d'attente et réveillez le suivant si nécessaire. un nœud approprié
private void unparkSuccessor (nœud de nœud)
Il convient de noter que si le prochain nœud n'est pas vide, débarrassez-le ; si le prochain nœud est nul, commencez par la queue et recherchez le nœud le plus en avant avec waitStatus<=0 qui n'est pas le nœud de la queue à l'avant.
La raison pour laquelle nous recherchons de l'arrière vers l'avant est que la définition du prédécesseur et du successeur d'une liste doublement chaînée n'est pas une opération atomique. Il peut arriver que le suivant d'un nœud soit vide mais soit déjà dans la file d'attente, ou que le nœud vienne tout juste de le faire. a été annulé et ses prochains points vers lui-même, et la traversée arrive juste à ce nœud.
public final void acquérirInterruptiblement (int arg)
Pour obtenir l'état de synchronisation en mode exclusif en réponse à une interruption, Thread.interrupted() sera utilisé pour déterminer s'il est interrompu avant tryAcqure. Si tel est le cas, une exception sera levée.
essayerAcquérir
essayez-le une fois d'abord
private void doAcquireInterruptably (int arg)
La logique de base est la même que celle de acquireQueued. La différence est que addWaiter est appelé en interne et ne renvoie pas s'il y a une interruption du thread. Au lieu de cela, il lève directement une InterruptedException après avoir vérifié que le parc est réveillé par l'interruption.
annulerAcquérir
public final boolean tryAcquireNanos (int arg, long nanosTimeout)
Acquisition exclusive de l'état de synchronisation avec timeout, répondant aux interruptions. Si l’état de synchronisation n’est pas obtenu dans le délai d’expiration, false est renvoyé.
essayerAcquérir
essayez-le une fois d'abord
booléen privé doAcquireNanos (int arg, long nanosTimeout)
La logique de base est la même que celle de doAcquireInterruptably, avec l'ajout d'une logique de jugement temporel et l'utilisation du seuil spinForTimeoutThreshold.
annulerAcquérir
version booléenne finale publique (int arg)
Le mode exclusif libère l’état de synchronisation. Tout d'abord, tryRelease est appelé pour essayer de modifier l'état. Après succès, il est jugé que head != null et head.waitStatus!=0 (le waitStatus de head est ici 0, indiquant une file d'attente vide lorsque le waitStatus de head). est 0, cela signifie que la file d'attente vide est appelée. UnparkSuccessor est appelé pour réveiller les nœuds suivants, puis renvoie true et tryRelease échoue et renvoie false ;
tryRelease booléen protégé (int arg)
Essayez de modifier l'état. Il n'y a pas d'implémentation spécifique dans AQS et les sous-classes doivent l'implémenter elles-mêmes. La variable arg de la méthode release est utilisée pour cette méthode ; la valeur de retour indique si l'état est libéré avec succès.
Pour une description d’arg, consultez la section d’implémentation ci-dessous.
déparquer le successeur
Mode de partage
Plusieurs threads obtiennent le statut de synchronisation en même temps
public final void acquireShared (int arg)
Le mode partagé acquiert l'état de synchronisation et ignore les interruptions. Implémentation : Appelez d'abord tryAcquireShared pour tenter d'obtenir l'état de synchronisation. Après un échec (le résultat est inférieur à 0), appelez doAcquireShared pour spin pour obtenir l'état de synchronisation.
protégé int tryAcquireShared (int arg)
Essayez d'obtenir l'état de synchronisation. Aucune méthode spécifique n'est fournie dans AQS et n'est implémentée par les sous-classes. Une valeur de retour inférieure à 0 indique que l'acquisition a échoué ; une valeur égale à 0 indique que l'acquisition a réussi, mais aucune autre acquisition en mode partagé n'a réussi ; une valeur supérieure à 0 indique que l'acquisition a réussi et que d'autres acquisitions en mode partage peuvent également réussir.
privé vide doAcquireShared (int arg)
Après avoir rejoint la file d'attente, spin essaie d'obtenir l'état de synchronisation. La logique de base est fondamentalement la même que celle de acquireQueued en mode exclusif. La différence est que lorsque le prédécesseur du nœud est head, le résultat renvoyé par tryAcquireShared doit être jugé If. il est supérieur ou égal à 0 (indiquant qu'il y a encore des ressources, peut continuer à se propager), puis appelez setHeadAndPropagate pour définir les attributs de tête et de propagation (définir une nouvelle tête et juger le nœud successeur, et le réveiller si approprié) ; sinon, vous devez toujours appeler ShouldParkAfterFailedAcquire et parkAndCheckInterrupt pour les opérations ultérieures (garage et jugement d'interruption ultérieur, etc.). De plus, la méthode selfTninterrupt pour définir l'état d'interruption est également exécutée dans cette méthode.
tryAcquireShared
Lorsque le prédécesseur du nœud est en tête, essayez-le d'abord.
private void setHeadAndPropagate (nœud de nœud, propagation int)
L'incarnation de la communication. Ce que cette méthode vérifie en réalité, c'est le nœud successeur du nœud qui acquiert actuellement le verrou, c'est-à-dire qu'il peut être propagé une fois vers l'arrière. Au début, je me demandais pourquoi je n'avais trouvé aucun code similaire au réveil en boucle. Plus tard, j'ai découvert que setHeadAndPropagate lui-même était dans for(;;). , donc cela atteint l'objectif de propagation vers l'arrière un par un. Notez qu’ils ne sont pas réveillés ensemble pour ensuite s’affronter, ils sont réveillés un par un.
Continuez à réveiller les nœuds à l’envers. Cette méthode ne sera appelée que lorsque tryAcquireShared renvoie le résultat r>=0 (indiquant qu'il y a encore des ressources disponibles) et que r est transmis à la méthode en tant que paramètre de propagation. Cette méthode définira le nœud sur head, puis déterminera s'il peut être transmis vers l'arrière. Si tel est le cas, appelez doReleaseShared pour réveiller le nœud suivant.
Logique du jugement : propager>0 (indiquant qu'il y a une autorisation) Ou tête précédente == null Ou head.waitStatus précédent < 0 Ou la tête actuelle (c'est-à-dire le nœud) == null Ou maintenant head.waitStatus < 0 Passez ensuite à l'étape suivante : noeud.suivant == null ou node.next.isShared()
En fait, ce n'est pas très clair : 1. Pourquoi devons-nous déterminer la tête actuelle ((h = tête) == null) 2. Pourquoi doReleaseShared doit encore être effectué après node.next == null
Spéculation de 1 : étant donné que head peut être modifié par d'autres threads pendant le processus de jugement, si le nouveau head remplit toujours cette condition, vous pouvez toujours continuer ; il semble que la logique de jugement ici ressemble davantage à "Je ne sais pas pourquoi head est nul", mais je l'essaie toujours. Une stratégie de traitement conservatrice, alors 2 peut être une stratégie conservatrice similaire.
On estime uniquement que waitStatus<0 est dû au fait que l'état de la tête peut changer en SIGNAL et que CONDITION n'apparaîtra pas ici.
vide privé doReleaseShared()
Spin réveille le nœud successeur, ce qui est différent du mode indépendant dans la mesure où il doit également garantir les propriétés de propagation. Implémentation : Boucle : Lorsque la file d'attente n'est pas vide, déterminez le waitStatus de head Lorsqu'il est égal à SIGNAL, CAS définit waitStatus sur 0. En cas de succès, unparkSuccessor(head) sera appelé pour réveiller le nœud successeur en cas d'échec. , recommencez depuis le début ; waitStatus est 0 et CAS définit waitStatus sur 0. Lorsque PROPAGATE échoue, recommencez ; puis jugez head==h pour voir si le nœud principal a été modifié, et si c'est le cas, quittez la boucle.
Notez que cette méthode ne transmet pas de paramètres et démarre directement depuis head.
Spéculation : le nœud en mode partagé ne peut avoir que trois états : 0 (juste créé), PROPAGATE et SIGNAL.
déparquer le successeur
annulerAcquérir
public final void acquireSharedInterruptably (int arg)
Le mode partagé obtient l'état de synchronisation et prend en charge les interruptions d'une manière similaire au mode exclusif.
tryAcquireShared
private void doAcquireSharedInterruptably (int arg)
Semblable à la logique doAcquireShared, une exception est levée après la détection d'une interruption
annulerAcquérir
public final booléen tryAcquireSharedNanos (int arg, long nanosTimeout)
Le mode partagé avec délai d'attente obtient l'état de synchronisation et prend en charge l'interruption.
tryAcquireShared
booléen privé doAcquireSharedNanos (int arg, long nanosTimeout)
La logique de jugement du délai d'attente est similaire au mode exclusif et les autres logiques d'exécution sont similaires à doAcquireShared.
annulerAcquérir
version booléenne finale publiqueShared (int arg)
Le mode partagé libère l'état de synchronisation
booléen protégé tryReleaseShared (int arg)
La méthode vide, implémentée par les sous-classes, tente de libérer l'état de synchronisation. arg est le paramètre de releaseShared et peut être défini par vous-même ; la valeur de retour est vraie lorsque cette méthode libère avec succès l'état de synchronisation et que d'autres threads peuvent l'acquérir, sinon elle renvoie faux.
doReleaseShared
Ici, nous ne réveillons toujours qu'un nœud suivant, et la propagation du réveil ultérieur est effectuée par le nœud réveillé.
Comment implémenter votre propre verrou basé sur AQS
ressources concurrentes
AQS utilise l'état de la variable privée pour représenter de manière abstraite les ressources pour la concurrence de verrouillage (par exemple, 1 signifie occupé, 0 signifie non occupé)
Les opérations sur l'état nécessitent l'utilisation de méthodes pertinentes : getState, setState, compareAndSetState, etc.
Méthode de mise en œuvre à la demande
tryAcquire, tryAcquireShared, tryRelease, tryReleaseShared, isHeldExclusively
Le paramètre arg de diverses méthodes dans AQS est finalement transmis à ces méthodes, donc ces méthodes déterminent si le paramètre arg est requis et comment l'utiliser. C'est la signification de « peut représenter n'importe quoi » dans les commentaires du code source.
Verrouillage exclusif (mode indépendant)
booléen protégé tryAcquire(int arg);
La mise en œuvre des requêtes et l'obtention de ressources compétitives sont généralement complétées par le fonctionnement du CAS de l'État. Les paramètres entrants peuvent être définis par le développeur.
Exemple simple : protected boolean tryAcquire(int acquiert) { assert acquires == 1; // Définit que le paramètre entrant ne peut être que 1 si (compareAndSetState(0, 1)) { renvoie vrai ; } renvoie faux ; } protected boolean tryRelease (versions int) { assert releases == 1; // Définit que le paramètre entrant ne peut être que 1 if (getState() == 0) lance une nouvelle IllegalMonitorStateException(); setState(0); renvoie vrai ; }
protégé booléen tryRelease(int arg);
Pour libérer des ressources concurrentes, la variable d'état est généralement décrémentée ou remise à zéro. Les paramètres entrants peuvent être définis par le développeur.
Il ne devrait y avoir aucun blocage ni attente à l'intérieur de la méthode
serrure partagée
protected int tryAcquireShared (int acquiert)
Les paramètres entrants sont définis par le développeur et peuvent être considérés comme le nombre de ressources qu'il souhaite obtenir ; la valeur de retour int peut être considérée comme le nombre restant de ressources partagées.
Exemple simple : protected int tryAcquireShared (int acquiert) {//non équitable pour (;;) { int disponible = getState(); int restant = disponible - acquiert ; if (restant < 0 || compareAndSetState (disponible, restant)) retour restant ; } } protégé booléen tryReleaseShared (versions int) { pour (;;) { int actuel = getState(); int next = versions actuelles ; if (compareAndSetState (actuel, suivant)) renvoie vrai ; } }
protégé booléen tryReleaseShared (versions int)
Les paramètres entrants sont définis par les développeurs eux-mêmes et peuvent être considérés comme le nombre de ressources qu'ils souhaitent libérer.
variable de condition
Bien que la logique d'implémentation d'AQS n'utilise pas l'état, lors de l'utilisation de variables de condition, l'état sera utilisé par défaut comme paramètre entrant de la version (int arg), il est donc souvent nécessaire de modifier la variable d'état lors de l'implémentation de ces méthodes.
booléen protégé isHeldExclusively()
Renvoie si le thread actuel occupe exclusivement le verrou mutex correspondant à la variable de condition
Méthodes de mise en œuvre courantes : booléen protégé isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } protected boolean tryAcquire(int acquiert) { ... setExclusiveOwnerThread(Thread.currentThread()); renvoie vrai ; }
RéentrantReadWriteLock
Verrouillage réentrant
Introduction
Les verrous réentrants sont divisés en deux méthodes : les verrous équitables et les verrous injustes.
implémenter l'interface
Verrouillage
Sérialisable
classe intérieure
classe statique abstraite Sync
La classe abstraite est la base de l'implémentation de ReetrantLock
hériter
RésuméQueuedSynchronizer
méthode principale
verrouillage vide abstrait()
Est-ce une méthode abstraite pour acquérir un verrou ? Doit-elle être implémentée par une sous-classe ?
final booléen non équitableTryAcquire (int acquiert)
Implémentation injuste de tryLock L'implémentation spécifique est la suivante : CAS acquiert le verrou lorsqu'aucun thread n'acquiert actuellement le verrou. En cas de succès, le thread exclusif au verrouillage est défini sur le thread actuel ; si le thread actuel détient déjà le verrou, le nombre de verrous actuels est mis à jour. Dans ces deux cas, true sera renvoyé, indiquant que le verrou est maintenu, et false sera renvoyé dans les autres cas.
Alors pourquoi une implémentation injuste serait-elle placée dans la classe parent Sync ? Étant donné que cette méthode est utilisée à la fois par les verrous équitables et par les verrous injustes, les tryLocks des deux sont basés sur cette implémentation.
tryRelease booléen final protégé (versions int)
L'implémentation de tryRelease dans AQS consiste à calculer d'abord le nombre total de verrous moins le nombre de verrous libérés pour obtenir le nombre de verrous détenus restants c. Si c est 0, définissez l'état sur la valeur de c et retournez. si aucun verrou n'est retenu.
booléen final protégé isHeldExclusively()
isHeldExclusivement implémenté dans AQS, return getExclusiveOwnerThread() == Thread.currentThread()
final int getHoldCount()
Renvoie le nombre de verrous détenus par le thread actuel. Cette méthode repose sur le jugement de isHeldExclusively
le booléen final isLocked()
return getState() != 0; Si un thread détient le verrou
Sujet final getOwner()
Renvoie le thread détenant le verrou, ou null sinon
ConditionObject final newCondition()
nouvelObjetCondition()
privé vide readObject (java.io.ObjectInputStream s)
Méthode de désérialisation, définir l'état sur 0
classe finale statique FairSync
Classe d'implémentation de verrouillage équitable
hériter
Synchroniser
méthode principale
verrouillage final du vide()
acquérir(1)
Le paramètre 1 a ici une signification pratique, différente de CountDownLatch.
tryAcquire booléen final protégé (int acquiert)
Implémentation TryAcquire du verrouillage équitable. Le processus d'implémentation est le suivant : lorsque le verrou occupé c est 0, s'il n'y a pas de thread devant et que cas définit avec succès c sur acquiert, alors définissez le thread actuel comme thread exclusif et retournez true si le thread actuel détient déjà le verrou, mettre à jour le nombre de verrous et renvoyer true ; renvoie false dans tous les autres cas ;
hasQueuedPrédécesseurs
Il y a plus de jugement ici qu'un verrouillage injuste. Même si aucun thread n'acquiert actuellement le verrou, s'il y a un thread qui attend plus longtemps, il abandonnera l'acquisition du verrou.
classe finale statique NonfairSync
Classe d'implémentation de verrouillage injuste
hériter
Synchroniser
méthode principale
verrouillage final du vide()
Essayez d'abord direct cas(0,1). Si cela réussit, cela signifie que personne d'autre n'est en train d'acquérir le verrou, puis définissez le thread exclusif comme thread actuel, s'il échoue, acquérez-le. (1)
En fait, la méthode tryAcquire du verrou injuste essaiera également cas. Le cas direct ici peut être de mettre en évidence l'injustice. Les appels antérieurs peuvent obtenir des ressources plus tôt.
tryAcquire booléen final protégé (int acquiert)
retourner non équitableTryAcquire(acquiert) L'implémentation injuste existe déjà dans Sync, appelez-la directement
Variables membres
synchronisation finale privée
Indique si le verrou est implémenté de manière juste ou injuste, et correspond à FairSync ou NonfairSync après instanciation.
Constructeur
public ReentrantLock()
Verrou injuste construit par défaut
public ReentrantLock (foire booléenne)
Construire un verrou équitable ou un verrou injuste selon un choix équitable
méthode principale
verrouillage public vide()
sync.lock()
public void lockInterruptiblement()
sync.acquireInterruptably(1)
public booléen tryLock()
return sync.nonfairTryAcquire(1); Si le verrou est acquis avec succès ou si le thread actuel détient déjà le verrou, il renvoie true, sinon il renvoie false.
Quel que soit le cadenas ici, il s’agit d’une tentative injuste d’acquérir le cadenas.
public boolean tryLock (long timeout, unité TimeUnit)
retourner sync.tryAcquireNanos(1, unit.toNanos(timeout))
tryAcquireNano appelle tryAcquire, il y a donc une différence entre l'équité et l'injustice.
déverrouillage du vide public ()
sync.release(1)
État public newCondition()
retourner sync.newCondition()
public int getHoldCount()
return sync.getHoldCount() Le nombre de verrous détenus par le thread actuel
public booléen isHeldByCurrentThread()
renvoie sync.isHeldExclusively() si le thread actuel détient le verrou
public booléen isLocked()
retourner sync.isLocked()
public final booléen isFair()
renvoyer l'instance de synchronisation de FairSync
Sujet protégé getOwner()
retourner sync.getOwner()
public final booléen hasQueuedThreads()
renvoie sync.hasQueuedThreads() s'il y a des threads dans la file d'attente
public final booléen hasQueuedThread (thread)
return sync.isQueued(thread); Si le thread actuel est dans la file d'attente
public final int getQueueLength()
return sync.getQueueLength();
Collection protégée<Thread> getQueuedThreads()
return sync.getQueuedThreads();
public booléen hasWaiters (Condition condition)
S'il y a des threads en attente sur la condition donnée (correspondant à la présence ou non de threads dans l'état CONDITION dans la file d'attente des conditions)
public int getWaitQueueLength (condition)
Le nombre de threads dans l'état CONDITION dans la file d'attente des conditions pour une condition donnée
Collection protégée<Thread> getWaitingThreads (Condition condition)
Condition
Introduction
Les variables de condition d'interface sont principalement utilisées pour gérer la dépendance de l'exécution du thread sur certains états, mapper les méthodes de surveillance de l'objet (wait/notify/notifyAll) pour diriger les objets et les combiner avec l'utilisation de n'importe quelle implémentation de Lock pour réaliser chaque objet a l'effet de plusieurs jeux d'attente. Cela équivaut à Lock remplaçant la synchronisation et à Condition remplaçant la méthode de surveillance, c'est-à-dire que Condition doit être utilisée conjointement avec Lock.
Les principales différences entre les méthodes Condition et Monitor sont : 1. La condition prend en charge plusieurs files d'attente et une instance de verrouillage peut être liée à plusieurs conditions. 2. La condition peut prendre en charge les paramètres d'interruption de réponse et de délai d'attente
La signification profonde de la liaison de plusieurs conditions est que les threads peuvent être gérés en fonction de différentes situations. Le contrôle dit « plus fin » signifie que les threads bloqués en raison de différentes conditions se trouvent dans des files d'attente de conditions différentes et ne seront réveillés que lorsqu'ils auront besoin d'être réveillés. . Entrer dans la file d'attente de synchronisation pour rivaliser, se différencie et isole.
comprendre
La différence avec la concurrence générale en matière de verrouillage est que la condition met l'accent sur les conditions. Dans un environnement multithread, les conditions doivent être remplies avant que certaines opérations puissent être effectuées, tandis que la concurrence générale en matière de verrouillage équivaut simplement à une compétition pour le droit d'exécuter un certain morceau de code ; .
Modèle de programmation habituel : Obtenez le verrouillage ; while (l'état conditionnel n'est pas satisfait) { déverrouiller le verrou ; Le thread se bloque et attend que la condition soit remplie pour la notification ; Réacquérir la serrure ; } Opérations des sections critiques ; déverrouiller le verrou ;
Relation ternaire : verrouillage, attente, assertion conditionnelle (prédicat). Chaque appel d'attente est implicitement associé à une assertion conditionnelle spécifique ; lors de l'appel d'attente pour une assertion conditionnelle spécifique, l'appelant doit déjà détenir le verrou de la file d'attente conditionnelle liée à l'assertion, et ce verrou doit protéger les conditions qui constituent l'assertion conditionnelle. Variables d'état
prédicat : une fonction qui renvoie le type booléen
créer
Créée via la méthode newCondition de l'interface Lock, la classe qui doit être implémentée implémente sa propre logique (créant essentiellement une instance de ConditionObject)
méthode principale
vide attendre()
Laissez le thread attendre jusqu'à ce qu'il soit réveillé par un signal ou une interruption ; lorsqu'il est appelé, le verrou associé à la condition sera automatiquement libéré et le thread cessera d'exécuter la tâche en cours jusqu'à ce qu'il soit réveillé par signal/signalAll/interruption/réveil parasite. Le thread doit réessayer d'acquérir le verrou associé à la condition avant d'appeler la méthode wait.
attente booléenne (longtemps, unité TimeUnit)
Semblable à waitNanos, vous pouvez spécifier l'unité de temps et renvoyer false lorsque le temps est écoulé, sinon renvoyer true
longue attenteNanos(long nanosTimeout)
Similaire à wait, mais se réveillera au plus tard après l'heure spécifiée et renverra l'heure inutilisée
void waitUninterruptably();
Similaire à wait, mais ne répond pas aux interruptions
booléen waitUntil (Date limite)
Semblable à waitNanos, la façon de spécifier l'heure est différente si la date limite est atteinte, elle renvoie false, sinon elle renvoie true.
signal vide()
Réveillez un thread en état d'attente. Une fois le thread réveillé, il doit acquérir à nouveau le verrou.
annuler signalAll()
Réveiller tous les threads en état d'attente
Verrouillage
Introduction
L'interface fournit des méthodes courantes pour demander/libérer des verrous
Comparaison avec synchronisé
Les mécanismes associés de synchronisation (tels que les objets de surveillance, etc.) sont intégrés et ne sont pas accessibles au niveau du langage. Lock est une solution alternative au niveau du langage, vous pouvez donc obtenir certaines commodités au niveau du langage (telles que la connaissance). si le verrou est acquis avec succès, etc.)
Le processus de verrouillage et de déverrouillage de synchronisé est complété par le mot-clé lui-même, tandis que Lock doit appeler explicitement la méthode de verrouillage et de déverrouillage.
Lock peut utiliser des variables de condition pour utiliser les verrous de manière plus flexible
Le verrouillage peut répondre aux interruptions, mais la synchronisation ne peut pas
méthode principale
verrou vide()
void lockInterruptiblement()
Les interruptions recevront une réponse pendant le processus de demande de verrouillage.
booléen tryLock()
Essayez d'acquérir le verrou et renvoyez le résultat directement (sans bloquer le thread actuel)
booléen tryLock (longtemps, unité TimeUnit)
tryLock avec délai d'attente et interruption de réponse
annuler le déverrouillage ();
Condition nouvelleCondition()
Renvoie un objet Condition associé au verrou
Support de verrouillage
Introduction
Primitives de base de blocage de thread (liées aux autorisations) utilisées pour créer des verrous et d'autres classes de synchronisation
La couche inférieure repose sur une implémentation non sécurisée
Méthode de construction
Privé, la construction d'objets de cette classe n'est pas autorisée
Variables membres
privé statique final sun.misc.Unsafe UNSAFE
Intégrer un bloc de code statique
sun.misc.Dangereux
privé statique final long parkBlockerOffset
Obtenez la position de décalage de l'attribut parkBlocker dans l'objet de classe Thread via Unsafe
parkBlocker est un enregistrement lorsque le thread est bloqué, ce qui facilite les outils de surveillance et de diagnostic pour identifier la raison pour laquelle le thread est bloqué ; il est nul lorsque le thread n'est pas bloqué ;
privé statique final long SEED
Obtenez la position de décalage de la propriété threadLocalRandomSeed dans la classe Thread via Unsafe
privé statique final long SONDE
Obtenez la position de décalage de la propriété threadLocalRandomProbe dans la classe Thread via Unsafe
privé statique final long SECONDAIRE
Obtenez la position de décalage de la propriété threadLocalRandomSecondarySeed dans la classe Thread via Unsafe
Ces trois propriétés sont définies par ThreadLocalRandom
méthode principale
Objet statique public getBlocker (Thread t)
Récupérez l'objet parkBlocker dans l'objet Thread via Unsafe
setBlocker de vide statique privé (Thread t, Object arg)
Méthode privée, définissez l'objet parkBlocker dans Thread via Unsafe, les méthodes suivantes de définition du bloqueur appellent cette méthode
parc vide statique public()
Bloquez le thread et attendez le "permis" pour continuer l'exécution.
Implémentation spécifique (Hotspot) : classe Parker
具体的实现与平台相关,每个线程都有一个Parker实例,在linux实现中实际上是以mutex和condition最终实现了锁和阻塞的过程(具体看笔记文章,这里不写了)
主要成员变量(linux实现)
private volatile int _counter
表示许可,大于0表示已获取许可,每次park只会将其设置为1,不会递增
Par conséquent, appeler deux fois unpark, puis appeler park deux fois, bloquera toujours la deuxième fois.
protected pthread_mutex_t _mutex[1]
互斥变量
在代码中可以通过linux原子指令pthread_mutex_lock、pthread_mutex_trylock、pthread_mutex_unlock对变量进行加锁和解锁操作
protected pthread_cond_t _cond[1]
条件变量
利用线程间共享的全局变量进行同步的一种机制,一般和互斥锁结合使用(避免条件变量的竞争);可以通过pthread_cond_wait、pthread_cond_timedwait等指令进行操作
Il existe également une file d'attente dans le noyau Linux pour gérer les processus bloqués
方法
park
unpark
Différences d'utilisation par rapport à l'attente
wait doit attendre la notification avant de pouvoir se réveiller. Il y a un processus séquentiel, et le déparcage attendu peut être appelé avant le parcage. À ce moment, le programme après le parcage peut continuer à s'exécuter.
attendre doit obtenir le moniteur de l'objet, mais pas se garer
Avis
Si une interruption se produit dans le parc, le parc reviendra également mais ne lancera pas d'exception d'interruption.
parc vide statique public (bloqueur d'objets)
En raison du rôle de parkBlocker, il est plus recommandé d'utiliser la méthode park avec les paramètres du bloqueur. Après l'avoir utilisée, vous pouvez voir l'invite sur quel objet le thread est bloqué via des outils de diagnostic tels que jstack, tels que le stationnement à attendre. <0x000000045ed12298> (un objet xxxx avec tout nom qualifié)
parc vide statique publicNanos (bloqueur d'objets, nanos longs)
parc vide statique publicNanos(long nanos)
La durée maximale pendant laquelle le programme peut être bloqué avant de poursuivre son exécution
public static void parkUntil (bloqueur d'objet, long délai)
parc vide statique publicJusqu'à (long délai)
public static void unpark (thread thread)