1. Proposer des problèmes de synchronisation
Supposons que nous utilisons un processeur double core pour exécuter deux threads A et B, Core 1 exécute le thread A, et Core 2 exécute le thread B, les deux threads doivent désormais ajouter 1 à la variable membre I de l'objet nommé OBJ. En supposant que la valeur initiale de I est 0, théoriquement, la valeur de I devrait devenir 2 après l'exécution des deux threads, mais en fait, il est très probable que le résultat soit 1.
Analyons les raisons maintenant. Par souci de simplicité de l'analyse, nous ne considérons pas la situation du cache. En fait, il y a un cache qui augmentera la possibilité que le résultat soit 1. Le thread A lit la variable I en mémoire dans l'unité d'opération arithmétique du noyau 1, puis effectue l'opération d'addition, puis écrit le résultat de calcul de retour à la mémoire. Parce que l'opération ci-dessus n'est pas une opération atomique, tant que le thread B lit la valeur de i en mémoire avant que le thread A écrit la valeur de i en ajoutant 1 à la mémoire (la valeur de i est 0 à l'heure actuelle), alors le résultat de I semblera certainement être 1. Parce que la valeur de I Lire par Threads A et B est 0, et la valeur après avoir ajouté 1 à 1, les deux threads écrivent 1 à la variable i, qui est, qui est.
La solution la plus courante consiste à utiliser le mot clé Synchronize pour verrouiller l'objet OBJ avec le code qui ajoute 1 au code I-visible en deux threads. Aujourd'hui, nous introduisons une nouvelle solution, qui est d'utiliser des classes connexes dans le package atomique pour le résoudre.
2. Prise en charge du matériel atomique
Dans un système de processeur unique (uniprocesseur), les opérations qui peuvent être effectuées en une seule instruction peuvent être considérées comme des "opérations atomiques" car les interruptions ne peuvent se produire que entre les instructions (car la planification du thread doit être terminée via les interruptions). C'est également la raison pour laquelle certains systèmes d'instructions CPU introduisent Test_and_Set, test_and_clear et autres instructions pour l'exclusion mutuelle de ressources critiques. Il est différent dans la structure symétrique multiprocesseur, puisque plusieurs processeurs fonctionnent indépendamment dans le système, même les opérations qui peuvent être effectuées dans une seule instruction peuvent être perturbées.
Sur la plate-forme x86, le CPU fournit les moyens de verrouiller le bus pendant l'exécution de l'instruction. Il y a un lead #hlockpin sur la puce CPU. Si le "verrouillage" du préfixe est ajouté à une instruction dans le programme de langage d'assemblage, le code de la machine d'assemblage entraînera la baisse du CPU du potentiel de #HlockPin lors de l'exécution de cette instruction et la libérer jusqu'à la fin de cette instruction, verrouillant ainsi le bus. De cette façon, d'autres processeurs sur le même bus ne peuvent pas accéder à la mémoire via le bus pour le moment, assurant l'atomicité de cette instruction dans un environnement multiprocesseur. Bien sûr, toutes les instructions ne peuvent pas être préfixées avec un verrouillage. Ajouter, ADC, et, BTC, BTR, BTS, CMPXCHG, DEC, Inc, Neg, Not, OR, SBB, Sub, XOR, XADD et XCHG peuvent être préfixés avec des instructions de "verrouillage" pour réaliser les opérations atomiques.
Le fonctionnement central de l'atomique est CAS (comparaison, implémenté à l'aide de l'instruction CMPXCHG, qui est une instruction atomique). Cette instruction a trois opérandes, la valeur de mémoire V de la variable (l'abréviation de la valeur), la valeur attendue actuelle de la variable (l'abréviation de l'exception), la valeur u de la variable souhaite mettre à jour (l'abréviation de la mise à jour). Lorsque la valeur de mémoire est la même que la valeur attendue actuelle, la valeur mise à jour de la variable est écrasée par la variable et le pseudo-code est exécuté comme suit.
if (v == e) {v = u return true} else {return false}Nous allons maintenant utiliser les opérations CAS pour résoudre les problèmes ci-dessus. Le thread B lit la variable I en mémoire dans une variable temporaire (en supposant que la valeur lue à l'heure actuelle est 0), puis lit la valeur de I dans l'unité d'opération arithmétique de Core1. Ensuite, ajoute 1 pour comparer si la valeur dans la variable temporaire est la même que la valeur actuelle de i. Si la valeur de I en mémoire est la même avec la valeur du résultat dans l'unité d'opération (c'est-à-dire i + 1) (notez que cette pièce est une opération CAS, il s'agit d'une opération atomique, qui ne peut pas être interrompue et que l'opération CAS dans d'autres threads ne peut pas être exécutée en même temps), sinon l'exécution d'instructions échoue. Si l'instruction échoue, cela signifie que le thread A a augmenté la valeur de I par 1. De là, nous pouvons voir que si la valeur de I Lire par les deux threads est 0 au début, alors l'opération CAS d'un thread peut réussir, car l'opération CAS ne peut pas être exécutée simultanément. Pour les threads qui ne parviennent pas à exécuter les opérations CAS, tant que les opérations CAS sont exécutées avec boucle, elle réussira certainement. Vous pouvez voir qu'il n'y a pas de blocage de fil, qui est essentiellement différent du principe de la synchronisation.
3. Introduction au package atomique et à l'analyse du code source
La caractéristique de base de la classe dans le package atomique est que dans un environnement multi-thread, lorsque plusieurs threads fonctionnent sur une variable unique (y compris les types de base et les types de référence) en même temps, il est exclusif, c'est-à-dire que lorsque plusieurs threads mettent à jour la valeur de la variable en même temps, et que le thread puisse réussir et que le thread non écarté peut continuer à essayer un verrouillage de spin, et à attendre jusqu'à ce que l'exécution soit réussie.
Les méthodes de base de la classe des séries atomiques appellent plusieurs méthodes locales dans la classe dangereuse. Nous devons d'abord savoir qu'une seule chose est la classe dangereuse, avec son nom complet: Sun.Misc.unsafe. Cette classe contient un grand nombre d'opérations sur le code C, y compris de nombreuses allocations directes de mémoire et appels d'opérations atomiques. La raison pour laquelle elle est marquée comme non sécurisée est de vous dire qu'un grand nombre d'appels de méthode dans ce domaine auront des risques de sécurité et devront être utilisés avec soin, sinon cela entraînera de graves conséquences. Par exemple, lors de l'allocation de la mémoire par dangereuse, si vous spécifiez vous-même certaines zones, cela peut faire franchir certains pointeurs comme C ++ pour franchir la limite à d'autres processus.
Les classes dans le package atomique peuvent être divisées en 4 groupes en fonction du type de données opérationnel.
AtomicBoolean,AtomicInteger,AtomicLong
Types de base des opérations atomiques pour le filetage
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
Fonctionnement atomique à filetage du type de tableau, qui ne fonctionne pas sur l'ensemble du tableau, mais sur un seul élément du tableau
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
Opérations en filet
AtomicReference,AtomicMarkableReference,AtomicStampedReference
Types de référence en filetage et opérations atomiques des types de référence qui empêchent les problèmes ABA
Nous utilisons généralement l'atomicinteger, l'atomicréférence et l'atomicstampeDrediference. Analyons maintenant le code source de l'ordre atomique dans le package atomique. Les codes source d'autres classes sont similaires en principe.
1. Constructeur de paramètre
public atomicInteger (int initialValue) {value = initialValue;}Comme le montre la fonction du constructeur, la valeur est stockée dans la valeur de la variable membre
valeur int privée volatile;
La valeur de la variable membre est déclarée de type volatile, qui montre la visibilité sous plusieurs threads, c'est-à-dire que la modification de tout thread sera immédiatement visible dans d'autres threads.
2. Méthode de composition de comparement (la valeur de la valeur est réalisée via ceci et ValueOffset interne)
Public Final Boolean ComparenseDset (int attend, int update) {return dangeta.compareAndWapint (this, valuoffset, attend, update);}Cette méthode est l'opération CAS la plus principale
3. Méthode GetandSet, dans laquelle la méthode de comparaison est appelée
public final int getAndSet (int newValue) {for (;;) {int current = get (); if (comparabledset (courant, newValue)) de retour courant; }}Si d'autres threads modifient la valeur de la valeur avant d'exécuter If (comparaison (Current, NewValue), la valeur de la valeur doit être différente de la valeur actuelle. Si le comparaison ne parvient pas à s'exécuter, vous ne pouvez que reproduire la valeur de la valeur, puis continuer à comparer jusqu'à ce qu'elle réussit.
4. Implémentation de I ++
public final int getAndIncrement () {for (;;) {int courant = get (); int next = courant + 1; if (comparabledset (courant, suivant)) Retour courant; }}5. Implémentation de ++ I
public final int incmenmentAndget () {for (;;) {int current = get (); int next = courant + 1; if (comparableDset (courant, suivant)) renvoie ensuite; }}4. Utiliser un exemple d'atomicInteger
Le programme suivant utilise AtomicInteger pour simuler le programme de vente de billets. Les deux programmes ne vendront pas le même billet dans le résultat de la course, et ils ne vendront pas les billets comme négatifs.
package javaleanning; import java.util.concurrent.atomic.atomicinteger; public class SellTicketS {atomicInteger ticket = new atomicInteger (100); class vendeur implémente runnable {@Override public void run () {while (ticket.get ()>> 0) {int tmpp = Tillets.get (); if (tickets.compareAndset (tmp, tmp-1)) {System.out.println (thread.currentThread (). GetName () + "" + tmp);}}}}} public static Void Main (String [] args) {sellTicketS ST = new SellTtickets (); New Thread (St.New (), SellTicket "Sellera"). Start (); nouveau thread (St.New Seller (), "SellerB"). Start ();}}5. Problème ABA
L'exemple ci-dessus exécute le résultat complètement correct. Ceci est basé sur le fait que deux (ou plus) threads fonctionnent sur des données dans la même direction. Dans l'exemple ci-dessus, les deux threads fonctionnent sur des billets en diminution. Par exemple, si plusieurs threads effectuent des opérations d'inscription d'objets sur une file d'attente partagée, les résultats corrects peuvent être obtenus via la classe atomicreference (c'est en fait le cas pour la file d'attente maintenue dans AQS). Cependant, plusieurs threads peuvent être inscrits ou radiés, c'est-à-dire que la direction de fonctionnement des données est incohérente, donc ABA peut se produire.
Prenons maintenant un exemple relativement facile à comprendre pour expliquer le problème ABA. Supposons qu'il existe deux threads T1 et T2, et ces deux threads effectuent des opérations d'empilement et d'empilement sur la même pile.
Nous utilisons la queue définie par atomicreference pour enregistrer la position supérieure de la pile
ATomicreference <T> Tail;
En supposant que le thread T1 est prêt à déploier, pour les opérations d'empilement, nous devons seulement mettre à jour la position supérieure de la pile du SP au journal à l'opération CAS, comme le montre la figure 1. Cependant, avant que le thread T1 ne exécute Tail.CompaEndset (SP, Newsp), le système effectue un planification du thread et le thread T2 commence l'exécution. T2 effectue trois opérations: A est hors de la pile, B est hors de la pile, puis A est sur la pile. À l'heure actuelle, le système recommence à planifier, et le thread T1 continue d'effectuer l'opération d'empilement, mais en vue du thread T1, l'élément en haut de la pile est toujours A (c'est-à-dire que T1 pense toujours que B est toujours le prochain élément en haut de la pile A), et la situation réelle est affichée dans la figure 2. T1. La pile est indiquée sur le nœud B. En fait, B n'existe plus dans la pile. Le résultat après T1 met un hors de la pile est illustré à la figure 3, ce qui n'est évidemment pas le résultat correct.
6. Solutions aux problèmes ABA
Utilisez ATOMICMARKABLEREFEREFER, ATOMICSTAMPEDREDREFENTE. Utilisez les deux classes atomiques mentionnées ci-dessus pour effectuer des opérations. Lors de la mise en œuvre de l'instruction de comparaison, ils ont non seulement besoin de comparer la valeur précédente et la valeur attendue de l'objet, mais ont également besoin de comparer la valeur de tampon actuelle (opération) et la valeur du tampon attendue (opération). Ce n'est que lorsque tout cela est vrai que la méthode de comparaison peut réussir. Chaque fois que la mise à jour est réussie, la valeur du tampon changera et le réglage de la valeur du tampon est contrôlé par le programmeur lui-même.
Public Boolean ComparandDset (v attendReference, v NewReference, int
À l'heure actuelle, la méthode comparentedSet nécessite quatre paramètres: attendu, newReference, attendStamp, newstamp. Lorsque nous utilisons cette méthode, nous devons nous assurer que la valeur du tampon attendue n'est pas la même que la valeur du tampon de mise à jour. Généralement newstamp = attendStamp + 1
Prenez les exemples ci-dessus
Supposons que le thread T1 soit avant la pile: SP pointe vers A et la valeur du tampon est de 100.
Le thread T2 s'exécute: après la sortie de A, SP pointe vers B et la valeur du tampon devient 101.
Une fois que B est libéré, SP pointe vers C et la valeur du tampon devient 102.
Une fois que A est placé dans la pile, SP pointe vers A et la valeur du tampon devient 103.
Le thread T1 continue d'exécuter l'instruction ComparandDset et constate que bien que SP pointe toujours vers A, la valeur attendue de la valeur du timbale 100 est différente de la valeur actuelle 103. Par conséquent, le comparaison échoue. Vous devez obtenir la valeur du journal (pour le moment, le journal indiquera C), et la valeur attendue de la valeur du timbre 103, puis effectuera à nouveau l'opération de comparaison. De cette façon, un déploie avec succès la pile, SP pointera vers C.
Notez que, puisque ComparendSet ne peut changer une seule valeur à la fois et ne peut pas modifier NewReference et NewStamp en même temps, pendant la mise en œuvre, une classe de paire est définie en interne pour transformer NewReference et NewStamp en un seul objet. Lorsque vous effectuez des opérations CAS, il s'agit en fait d'une opération sur l'objet paire.
Paire de classe statique privée <T> {référence finale T; Tampon INT final; paire privée (t référence, int tampon) {this.reference = référence; this.stamp = tampon; } statique <T> paire <T> de (t référence, int tampon) {return new pair <T> (référence, tampon); }}Pour atomicmarkableRereference, la valeur du tampon est une variable booléenne, tandis que la valeur du tampon dans ATOMICSTAMPEDREDERFEFER est une variable entière.
Résumer
Ce qui précède concerne la brève discussion de cet article sur les principes de mise en œuvre et les applications des packages atomiques en Java. J'espère que ce sera utile à tout le monde. Les amis intéressés peuvent continuer à se référer à d'autres sujets connexes sur ce site. S'il y a des lacunes, veuillez laisser un message pour le signaler.