Nous savons que les opérations simultanées implémentées par Java doivent enfin être terminées par notre CPU. En attendant, nous avons compilé le code source Java en fichier .class, puis chargé, puis exécuté par le moteur d'exécution de la machine virtuelle, interprété comme un langage d'assemblage, puis converti en instructions du système d'exploitation, puis convertis en 1, 0, et enfin le processeur est reconnu et exécuté.
Lorsque nous mentionnons la concurrence de Java, nous ne pouvons pas nous empêcher de penser à des mots clés communs en Java: volatile et synchronisé. Ensuite, nous les analyserons à partir de ces deux mots d'arrêt:
Le principe de mise en œuvre sous-jacent de la volatile
Principes de mise en œuvre et applications de synchronisés
volatil
En parlant de volatile, l'intervieweur est la question préférée à poser dans les interviews de Java. Lorsque nous le voyons, la première chose à laquelle nous pensons est de maintenir la visibilité entre les fils. Il s'agit d'un léger synchronisé, qui dans certains cas peut remplacer la synchronisation.
Le rôle de volatile:
Pour une variable modifiée par volatile, le modèle de mémoire Java garantira que les valeurs variables observées par tous les threads sont cohérentes.
Comment fonctionne volatile:
Nous pouvons définir une variable volatile, attribuer des valeurs et utiliser des outils pour obtenir les instructions d'assemblage générées par le compilateur JIT. Nous constaterons que lors de l'écriture de la variable volatile, il y aura une instruction supplémentaire: une instruction préfixée avec verrouillage:
L'instruction de préfixe de verrouillage fait revenir deux choses au processeur multi-core:
① Risez les données de la ligne de cache de processeur actuelle en mémoire.
②Ce fonctionnement de la mémoire d'écriture invalidera l'adresse de mémoire en cache des données dans d'autres processeurs.
Lorsque nous connaissons les deux points ci-dessus, il n'est pas difficile pour nous de comprendre le mécanisme de la variable Volatie.
Sous plusieurs processeurs, afin de s'assurer que le cache de chaque processeur est cohérent, un protocole de cohérence du cache sera mis en œuvre. Chaque processeur renifle les données propagées sur le bus pour vérifier si la valeur en cache a expiré.
synchronisé
En pensant à la concurrence multi-thread, la première chose à laquelle je pense est synchronisée. Traduit par synchronisation. Nous savons tous que c'est une serrure lourde. Lorsque vous l'utilisez pour une méthode ou un bloc de code, lorsqu'un thread obtient cette serrure, d'autres threads tomberont dans un état suspendu, qui apparaîtra dans l'état de sommeil en Java. Nous savons tous que la suspension et le temps d'exécution du fil doivent être transférés à l'état du noyau du système d'exploitation (le correspondant à l'état du noyau est l'état utilisateur), qui est particulièrement inutile des ressources CPU, donc ce verrouillage des poids lourds est véritable!
Cependant, après Java SE 1.6, l'équipe de maintenance de Java a effectué une série d'optimisations à ce sujet (ces optimisations sont discutées un par un), donc ce n'est pas que "lourd", et le verrou réentrant, qui avait des avantages dans le passé, est devenu moins avantageux (Reentrantlock).
Parlons de synchronisé dans les aspects suivants:
Les bases de la synchronisation pour atteindre la synchronisation
Comment le verrouillage des outils synchronisés
Verrouillage positif, verrouillage léger (verrouillage de rotation), verrouillage des poids lourds
Mise à niveau de verrouillage
Comment mettre en œuvre des opérations atomiques en Java
①Les bases de synchronisées pour atteindre la synchronisation:
Nous pouvons voir synchronisé dans le développement ou dans le code source Java, tel que HashTable, StringBuilder et d'autres endroits. Il existe deux façons courantes:
Ⅰ, méthode de synchronisation
La méthode de synchronisation ne doit être synchronisée que avant la méthode. Lorsqu'un thread l'exécute, d'autres threads tomberont en attente jusqu'à ce qu'il libère le verrou. L'utilisation des méthodes peut être divisée en deux types: pour les méthodes de synchronisation ordinaires et pour les méthodes statiques. La différence entre eux est que les objets verrouillés sont différents. La position verrouillée des méthodes ordinaires est l'objet actuel, et la position verrouillée des méthodes statiques est l'objet de classe de la classe actuelle.
Ⅱ, bloc de méthode de synchronisation
Le bloc de méthode de synchronisation verrouille l'objet configuré dans les supports après synchronisation. Cet objet peut être une valeur et toute variable ou objet.
②Comment des outils synchronisés Lock:
Dans la spécification JVM, vous pouvez voir le principe de mise en œuvre de synchronisé dans JVM. JVM met en œuvre la synchronisation des méthodes de synchronisation et des blocs de code en fonction de la saisie et de la sortie de l'objet Monitor. Le bloc de code est implémenté à l'aide d'instructions de surveillance et de monitorexit. La méthode de synchronisation n'est pas spécifiquement donnée dans la spécification JVM. Cependant, je crois que les principes spécifiques devraient être différents. Ce n'est rien de plus que de compiler le code source Java dans un fichier de classe et de marquer la méthode synchronisée dans le fichier bytecode de classe. Cette méthode sera synchronisée lorsque le moteur bytecode exécutera cette méthode.
Verrouillage de déflexion, verrouillage léger (verrouillage de rotation), verrouillage des poids lourds:
Avant de parler de serrures, nous devons connaître l'en-tête de l'objet Java et l'en-tête de l'objet Java:
Le verrou utilisé par synchronisé est stocké dans l'en-tête de l'objet Java. L'en-tête d'objet Java a 32 bits / 64 bits (en fonction du nombre de bits du système d'exploitation) Longueur Markword stocke le HashCode et le verrouillage des informations de l'objet. Il y a des espaces à 2 bits dans Markword pour représenter l'état de la serrure 00, 01, 10, 11, respectivement, représentant des verrous légers, des verrous de biais, des verrous poids lourds et des marques GC.
Verrouillage positif: le verrouillage positif est appelé verrouillage excentrique. D'après le nom, nous pouvons voir qu'il s'agit d'une serrure qui tend vers un certain fil.
Dans le développement réel, nous avons constaté que la concurrence multi-thread, la plupart des méthodes de synchronisation sont réalisées par le même thread, et la probabilité que plusieurs threads rivalisent pour une méthode sont relativement faibles, donc l'acquisition et la libération de serrures provoqueront de nombreux déchets de ressources. Par conséquent, afin de faire en sorte que le fil obtienne un verrou à un coût inférieur, un verrouillage de biais est introduit. Lorsqu'un thread accède à un bloc de synchronisation et acquiert un verrou, l'ID de thread du verrouillage de polarisation sera stocké dans l'enregistrement de verrouillage dans la trame de pile de l'en-tête et du fil d'objet. À l'avenir, lorsque le thread entre et quitte le bloc de synchronisation, il n'a pas besoin d'effectuer des opérations CAS pour verrouiller et déverrouiller. Il est seulement nécessaire de simplement vérifier s'il existe un verrouillage de biais pointant vers le markword actuel dans l'en-tête de l'objet (dans Markword, il existe un bit d'indicateur de verrouillage de biais pour indiquer si l'objet actuel prend en charge le verrouillage du biais. Nous pouvons utiliser le paramètre JVM pour définir le verrouillage de biais).
En ce qui concerne la libération de verrous biaisés, les verrous biaisés utilisent le mécanisme de libération du verrou jusqu'à ce que la compétition existe, de sorte que le fil qui maintient le verrou biaisé libèrera le verrou lorsque d'autres threads essaient de rivaliser pour les verrous biaisés.
Remarque: dans Java6, 7, le verrouillage du biais est démarré par défaut
Verrouillage léger:
Un verrou léger est qu'avant d'exécuter le bloc de synchronisation, JVM créera un espace pour stocker l'enregistrement de verrouillage dans la trame de pile du thread actuel et copiera le markword dans l'en-tête d'objet. Ensuite, le thread essaiera de remplacer le markword dans l'en-tête de l'objet par un pointeur vers l'enregistrement de verrouillage. S'il réussit, le thread actuel obtient le verrou. S'il échoue, cela signifie que d'autres threads rivalisent pour le verrou et que le fil actuel tourne pour obtenir le verrou.
④ Mise à niveau de verrouillage:
Si le fil actuel ne peut pas essayer la méthode ci-dessus pour obtenir le verrou, cela signifie que le verrouillage actuel est en concurrence et que le verrou sera mis à niveau vers un verrou de poids lourd.
La différence entre le verrouillage léger et le verrouillage biaisé:
Les verrous légers utilisent les opérations CAS pour éliminer les mutex utilisés dans la synchronisation sans concurrence, tandis que les verrous biaisés éliminent toute la synchronisation sans concurrence sans concurrence, et même les opérations CAS ne sont pas effectuées!
⑤ Comment mettre en œuvre des opérations atomiques en Java:
Avant de comprendre comment Java implémente les opérations atomiques, nous devons savoir comment les processeurs mettent en œuvre les opérations atomiques:
Les processeurs sont généralement divisés en deux façons d'effectuer des opérations atomiques: verrouillage du cache et verrouillage des bus, parmi lesquels le verrouillage du cache est meilleur, tandis que le verrouillage du bus est plus consommateur de ressources. (Nous n'expliquerons pas trop les deux méthodes de verrouillage ici, mais il y aura des explications détaillées dans le système d'exploitation)
Java utilise (principalement) la boucle CAS pour mettre en œuvre des opérations atomiques, mais l'utilisation de CAS pour implémenter les opérations atomiques entraînera également certains des problèmes classiques suivants:
1) Problème ABA
La classe atomicstampeDreDeference est fournie dans JDK pour résoudre (fournir des références attendues et des drapeaux attendus)
2) Temps de cycle long et frais généraux élevés
Je ne peux pas résoudre ceci, c'est un problème courant de circulation
3) Seules les opérations atomiques d'une variable partagée peuvent être garanties
Une atomique est fournie dans JDK pour résoudre le problème, plaçant plusieurs variables partagées dans une classe pour les opérations CAS.