Dans le code du programme C, nous pouvons utiliser le verrouillage mutex fourni par le système d'exploitation pour obtenir un accès mutex aux blocs synchrones et aux travaux de blocage et de réveil. Cependant, en Java, en plus de fournir LockAPI, des mots clés synchronisés sont également fournis au niveau de la syntaxe pour implémenter les primitives de synchronisation Mutex. Alors, comment implémentez-vous la clé synchronisée dans le JVM?
1. Représentation de bytecode synchronisée:
Il existe deux syntaxes synchronisées intégrées dans le langage Java: 1. Instructions synchronisées; 2. Méthode synchronisée. Pour les instructions synchronisées lorsque le code source Java est compilé en bytecode par Javac, MonitorFer et Monitorexit ByteCode Les instructions seront insérées respectivement aux positions d'entrée et de sortie du bloc de synchronisation. La méthode synchronisée sera traduite en instructions d'appel et de retour de méthode ordinaire telles que: Instructions invokevirtual et areturn. Il n'y a aucune instruction spéciale au niveau des bytecodes VM pour implémenter la méthode modifiée synchronisée. Au lieu de cela, la position de l'indicateur synchronisé 1 dans le champ Méthode Access_Flags de la méthode est placée dans le tableau de la méthode du fichier de classe, indiquant que la méthode est une méthode synchronisée et utilise l'objet qui appelle la méthode ou la classe appartenant à la méthode pour représenter Klass comme un objet de verrouillage.
2. Optimisation des verrous dans JVM:
En termes simples, le moniteur et le bytecode monitorexit dans JVM s'appuient sur Mutexlock du système d'exploitation sous-jacent pour l'implémenter. Cependant, comme l'utilisation de Mutexlock nécessite de suspendre le thread actuel et de passer de l'état utilisateur à l'état du noyau à exécuter, cette commutation est très coûteuse; Cependant, dans la plupart des cas en réalité, la méthode de synchronisation est exécutée dans un environnement unique (environnement de compétition sans serrure). Si Mutexlock est appelé à chaque fois, cela affectera sérieusement les performances du programme. Cependant, dans JDK1.6, de nombreuses optimisations ont été introduites à la mise en œuvre des verrous, tels que le grossissement des verrous, l'élimination des verrous, le verrouillage léger, le verrouillage biaisé, le filage adaptatif et d'autres technologies pour réduire les frais généraux de l'opération de verrouillage.
Comence-verrou: c'est-à-dire réduire les opérations de verrouillage et de verrouillage inutiles, élargissez plusieurs verrous continus dans une serrure avec une gamme plus grande.
Élimination des verrous: grâce à l'analyse d'évasion par le compilateur JIT d'exécution, une certaine protection de verrouillage est éliminée. Certaines données ne sont pas partagées par d'autres threads en dehors du bloc de synchronisation actuel. Grâce à l'analyse d'évasion, l'espace des objets peut être alloué sur la pile locale de fil (en même temps, il peut également réduire les frais généraux de collecte des ordures sur le tas).
Lightwerking: La mise en œuvre de ce verrou est basée sur l'hypothèse que dans les cas réels, la plupart du code de synchronisation de notre programme se trouve généralement dans un état de concurrence sans serrure (c'est-à-dire un environnement d'exécution unique). Dans le cas d'une concurrence sans serrure, il peut éviter complètement d'appeler les mutex poids lourds au niveau du système d'exploitation. Au lieu de cela, dans le surveillant et le monitorexit, il vous suffit de vous fier à une instruction atomique CAS pour terminer l'acquisition et la libération de la serrure. En cas de concurrence de verrouillage, le fil qui ne parvient pas à exécuter les instructions CAS appellera le système de fonctionnement Mutex pour entrer dans l'état de blocage et se réveiller lorsque le verrou est libéré (les étapes de traitement spécifiques sont discutées en détail ci-dessous).
Biais-bloquant: c'est pour éviter d'exécuter des instructions atomiques CAS inutiles lors de l'acquisition de serrures dans le cas d'une concurrence sans serrure, car bien que les instructions atomiques CAS soient relativement faibles en coût par rapport aux serrures poids lourds, ils ont toujours des retards locaux très considérables (voir cet article).
Spinning adaptatif: Lorsqu'un fil ne fait pas d'opération de CAS lors de l'acquisition d'un verrou léger, il entrera une attente occupée avant d'entrer dans le verrouillage des poids lourds du système d'exploitation (MutexSemaphore) associé au moniteur, puis réessayer. S'il échoue toujours après un certain nombre de tentatives, le sémaphore associé au moniteur (c'est-à-dire le verrouillage de mutex) est appelé pour entrer dans l'état de blocage.
3. OBJECTHEADER:
Lors de la création d'un objet dans le JVM, deux en-têtes d'objet de taille mot seront ajoutés devant l'objet. Un mot sur la machine 32 bits est de 32 bits. Différents contenus sont stockés dans MarkWorld en fonction de différents bits d'état. Comme le montre la figure ci-dessus, dans une serrure légère, Markword est divisé en deux parties. Au début, Lockword est défini sur HashCode, et les trois bits les plus bas représentent l'état où se trouve le mot de verrouillage. L'état initial est 001 représente l'état sans serrure. Klassptr pointe vers l'adresse représentée par l'objet dont les bytecodes de classe se trouvent à l'intérieur de la machine virtuelle. Les champs représentent des champs d'instance d'objet continu.
4. MonitorRecord:
MonitorRecord est une structure de données privée de threads. Chaque thread a une liste des monitorrecords disponibles et une liste mondiale disponible. Alors, quelles sont les utilisations de ces monitorrecords? Chaque objet verrouillé sera associé à un monitorrecord (le mot de verrouillage dans l'en-tête de l'objet pointe vers l'adresse de début de monitorrecord. Étant donné que cette adresse est alignée sur 8 octets, les trois bits les plus bas de mot de verrouillage peuvent être utilisés comme bits d'état). Dans le même temps, il y a un champ de propriétaire en monitorrecord pour stocker l'identifiant unique du fil qui possède le verrou, indiquant que la serrure est occupée par ce fil. La figure suivante montre la structure interne de monitorrecord:
Propriétaire: NULL au début signifie qu'aucun fil ne possède actuellement l'enregistrement du moniteur. Lorsque le thread possède avec succès le verrou, il enregistre l'identité unique du fil et lorsque le verrou est libéré, il est défini sur NULL;
EntryQ: Associez un système System (Semaphore) pour bloquer tous les threads qui ne verrouillent pas l'enregistrement du moniteur.
RCTHIS: représente le nombre de tous les threads bloqués ou en attente sur l'enregistrement du moniteur.
Nest: utilisé pour implémenter le comptage des verrous de rentrée.
HashCode: enregistre la valeur de code de hashcode copiée à partir de l'en-tête de l'objet (peut également contenir l'âge GC).
Candidat: utilisé pour éviter le blocage inutile ou en attendant que les threads se réveillent, car un seul thread peut posséder avec succès le verrou à chaque fois. Si le fil précédent qui libère la serrure réveille tous les fils de blocage ou d'attente à chaque fois, il provoquera une commutation de contexte inutile (du blocage au prêt, puis à un blocage à nouveau en raison de la défaillance du verrouillage de la compétition) et conduira ainsi à une dégradation sévère des performances. Le candidat n'a que deux valeurs possibles: 0 signifie qu'il n'y a pas de thread qui doit être réveillé jusqu'à 1 signifie réveiller un thread successeur pour rivaliser pour le verrou.
5. Mise en œuvre spécifique des verrous légers:
Un thread peut verrouiller un objet de deux manières: 1. Obtenez le verrou de l'objet en développant un objet dans un état sans serrure (statut bit 001); 2. L'objet est déjà dans un état élargi (bit d'état 00) mais le champ du propriétaire de l'enregistrement du moniteur indiqué par Lockword est nul, vous pouvez donc essayer directement de définir le propriétaire sur sa propre identité via l'instruction atomique CAS pour obtenir le verrou.
Le processus général d'obtention d'une serrure (surveillant) est le suivant:
(1) Lorsque l'objet est dans un état sans serrure (la valeur de mot de disques est HashCode, le bit d'état est 001), le thread obtient d'abord un enregistrement de moniteur gratuit à partir de sa liste d'enregistrements de moniteur disponible. Le nid initial et les valeurs du propriétaire sont prédéfinis à 1 et respectivement la propre identification du thread. Une fois que l'enregistrement du moniteur est prêt, nous installons l'adresse de démarrage de l'enregistrement du moniteur sur le champ de mot de verrouillage de l'en-tête de l'objet à travers l'instruction atomique CAS pour se développer (le texte d'origine est gonflé. Je pense que la raison pour laquelle elle est appelée gonfle est principalement parce que l'objet est élargi une fois qu'il est élargi; pour l'efficacité spatiale, le moniteur est utilisé pour étendre la taille de l'objet; la structure enregistrée est terminée à partir de l'efficacité de l'objet et uniquement attaché pour l'objet. contradictoire avec ce document.
(2) L'objet a été élargi et le thread enregistré dans le propriétaire est identifié comme le thread qui acquiert la serrure elle-même. C'est le cas des écluses réentrantes. Il vous suffit d'ajouter simplement 1 au nid. Aucune opération atomique n'est requise et très efficace.
(3) L'objet a été élargi mais la valeur du propriétaire est nul. Cet état se produit lorsqu'un blocage ou une attente sur un serrure est verrouillé en même temps, le propriétaire précédent de la serrure vient de libérer le verrou. À l'heure actuelle, plusieurs threads essaient de mettre le propriétaire sur leur propre identité grâce à l'instruction atomique CAS pour obtenir le verrou dans un état de concurrence multi-thread. Le thread qui ne parvient pas à concurrencer entrera le chemin d'exécution du quatrième cas (4).
(4) L'objet est dans un état élargi et le propriétaire n'est pas nul (verrouillé). Il tourne un certain nombre de fois avant d'appeler la mutex poids lourd du système d'exploitation. Lorsqu'un certain nombre de fois sont atteints, si le verrou n'est toujours pas obtenu avec succès, il est temps de commencer à entrer dans l'état de blocage. Tout d'abord, ajoutez la valeur de RFThis atomiquement par 1. Étant donné que d'autres threads peuvent détruire la relation entre l'objet et l'enregistrement du moniteur pendant l'ajout de 1, il est nécessaire d'effectuer une autre comparaison après l'ajout de 1 pour s'assurer que la valeur de mot de verrouillage n'a pas été modifiée. Lorsqu'il est constaté qu'il a été modifié, le processus de surveillant doit être répété. En même temps, on observe à nouveau si le propriétaire est nul. Si c'est le cas, il appellera CAS à participer au verrouillage du concours. Si la compétition de verrouillage échoue, elle entrera dans un état de blocage.
Le processus général de libération de la serrure (monitorexit) est le suivant:
(1) Vérifiez d'abord si l'objet est dans un état élargi et le thread est le propriétaire du verrou. S'il est constaté qu'il est faux, une exception sera lancée;
(2) Vérifiez si le champ de nid est supérieur à 1. S'il est supérieur à 1, réduisez simplement le nid de 1 et continuez à avoir la serrure. S'il est égal à 1, entrez l'étape (3);
(3) Vérifiez si RFTHIS est supérieur à 0, définissez le propriétaire NULL et réveillez un fil de blocage ou d'attente pour essayer d'acquérir le verrou à nouveau. S'il est égal à 0, il entrera l'étape (4)
(4) Déflasger un objet, libérer le verrouillage en remplaçant le mot de verrouillage de l'objet à la valeur de code de hash d'origine pour libérer le verrouillage et remettre l'enregistrement du moniteur sur le thread.
Résumer
Référence: " Compréhension approfondie des caractéristiques avancées et des meilleures pratiques de Java Virtual Machine JVM (Zhou Zhiming) "
Ce qui précède est l'intégralité du contenu de cet article sur l'analyse des problèmes synchronisés et de mise en œuvre des détails JVM. J'espère que ce sera utile à tout le monde. S'il y a des lacunes, veuillez laisser un message pour le signaler. Merci vos amis pour votre soutien pour ce site!