Décrypter le mécanisme de verrouillage en multi-threading Java : les principes de fonctionnement et les stratégies d'optimisation de CAS et Synchronized

CAS

Qu'est-ce que le CAS

CAS : Le nom complet est Compare and swap, signifiant littéralement : "Comparer et swap". CAS implique les opérations suivantes :
Supposons que les données d'origine dans la mémoire sont A, que l'ancienne valeur attendue est B et que la valeur qui doit être modifié est C.

  1. Comparez d’abord A et B pour voir si A et B sont identiques.
  2. Si A et B sont identiques, attribuez la valeur des données C à A.
  3. Opération de retour réussie.

Écrivons un pseudocode CAS pour nous aider à mieux comprendre CAS.

 boolean Cas(int a,int b,int c){
    
    
        //进行比较看a是否发生变化
        if(a==b){
    
    
            a=c;
            return true;
        }
       return false;
    }

CAS est une méthode d'implémentation de verrouillage optimiste. Lorsque plusieurs threads opèrent sur une donnée, un seul thread fonctionne avec succès, et les autres threads ne bloqueront pas et renverront un signal indiquant que l'opération a échoué.
Le vrai CAS est complété par une instruction matérielle atomique. Ce n'est que lorsque le matériel le prend en charge que le logiciel peut être implémenté.

Application du CAS

La bibliothèque standard fournit le package java.util.concurrent.atomic et les classes qu'il contient sont toutes implémentées sur la base de cette méthode.
Un exemple typique est la classe AtomicInteger, dans laquelle getAndIncrement est équivalent à l'opération i++.

public static void main(String[] args) {
    
    
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        AtomicInteger  seq = new AtomicInteger(0);
        //进行++操作
        seq.getAndIncrement();
        seq.getAndIncrement();
        seq.getAndIncrement();
        System.out.println(seq);
    }

Insérer la description de l'image ici
Lorsque l'on clique sur la méthode d'auto-incrémentation, on voit que son fonctionnement est également implémenté via le pseudocode ci-dessus.
Insérer la description de l'image ici
Vous pouvez également utiliser CAS pour implémenter des verrous tournants

problème ABA

Supposons qu'il y ait deux threads t1 et t2. Il existe une variable partagée num avec une valeur initiale de A.
Ensuite, le thread t1 veut utiliser CAS pour changer la valeur numérique en Z, il a alors besoin

  • Lisez d’abord la valeur de num et enregistrez-la dans la variable oldNum.
  • Utilisez CAS pour déterminer si la valeur actuelle de num est A. Si c'est A, remplacez-la par Z.
    Cependant, entre l'exécution de ces deux opérations par t1, le thread t2 peut changer la valeur de num de A à B, et de B à A à nouveau.

Exemples d'exceptions

Prenons l'exemple du retrait d'argent de la banque :

  1. Dépôt de 100, le fil 1 obtient que la valeur actuelle du dépôt soit de 100 et s'attend à ce qu'elle soit mise à jour à 50 ; le fil 2 obtient que la valeur actuelle du dépôt soit de 100 et s'attend à ce qu'elle soit mise à jour à 50.
  2. Le fil 1 effectue la déduction avec succès et le dépôt passe à 50. Le fil 2 est bloqué et en attente.
  3. Avant que le fil 2 ne soit exécuté, votre ami vous en transfère exactement 50 et le solde du compte devient 100.
  4. C'est au tour du Thread 2 de s'exécuter, et on constate que le dépôt actuel est de 100, ce qui est le même que les 100 lus auparavant, et l'opération de déduction est à nouveau effectuée.

De cette façon, notre argent disparaîtra, cette situation est donc absolument inacceptable.

Nous avons donc introduit des numéros de version pour résoudre ce problème. CAS lit également le numéro de version lors de la lecture de l'ancienne valeur. Lors de la modification, si le numéro de version lu est le même que le numéro de version actuel, la modification sera effectuée. Si le numéro de version actuel est supérieur au numéro de version lu, la modification échouera.

Principe synchronisé

Caractéristiques de base

  1. Cela commence par un verrouillage optimiste, et si le conflit de verrouillage est grave, il évolue vers un verrouillage pessimiste.
  2. Synchronisé est un verrou réentrant.
  3. C'est un verrouillage injuste.
  4. C'est un verrou non lisible et inscriptible
  5. Cela commence par la mise en œuvre d’un verrou léger, et si le verrou est maintenu pendant une longue période, il est converti en un verrou lourd.

Processus de verrouillage

Organigramme de verrouillage :
Insérer la description de l'image ici

verrouillage de biais

Le verrouillage biaisé consiste à marquer dans l'objet de verrouillage actuel à quel thread appartient le verrou. Aucun verrouillage réel n'est effectué. Si cela peut être effectué sans verrouillage, il ne sera pas verrouillé, ce qui réduit la surcharge inutile. Uniquement lorsque d'autres threads sont en compétition pour le verrou, la serrure sera améliorée. , de la serrure biaisée à la serrure légère.

serrure légère

Une fois le verrou mis à niveau vers un verrou léger, il est implémenté via CAS.

  • Vérifiez et mettez à jour un bloc de mémoire via CAS.
  • Si la mise à jour réussit, le verrouillage est considéré comme réussi.
  • Si la mise à jour échoue, on considère que le verrou a échoué et que le verrou est occupé.

Serrure lourde

Si la compétition devient plus intense et que le spin ne peut pas obtenir rapidement le statut de verrouillage, il se transformera en un verrou lourd.
Le verrou lourd fait ici référence à l'utilisation du mutex fourni par le noyau.

  • Pour effectuer l'opération de verrouillage, entrez d'abord dans l'état du noyau.
  • Déterminer si le verrou actuel est occupé en mode noyau
  • Si la serrure n'est pas occupée, le verrouillage est réussi et le système repasse en mode utilisateur.
  • Si la serrure est occupée, la serrure échoue. À ce moment-là, le thread entre dans la file d'attente pour le verrou et se bloque. En attente d'être réveillé par le système d'exploitation.
  • Après une série de vicissitudes de la vie, le verrou a été libéré par d'autres threads. Le système d'exploitation s'est également souvenu du thread suspendu, il a donc réveillé le thread et a tenté de réacquérir le verrou.

Lorsque plusieurs threads sont en compétition pour le même verrou et que le temps d'attente de rotation est trop long et que le verrou ne peut pas être obtenu, la JVM met à niveau le verrou vers un verrou lourd. À ce stade, le thread ne tourne plus et n'attend plus, mais entre dans l'état du noyau et gère l'état de verrouillage et la file d'attente via l'implémentation mutex fournie par le système d'exploitation.
En mode noyau, le système d'exploitation détermine si le verrou actuel est déjà occupé. Si le verrou n'est pas occupé, le thread acquiert avec succès le verrou et repasse en mode utilisateur pour continuer l'exécution. Si le verrou est déjà occupé, le thread ne parvient pas à se verrouiller. À ce moment-là, le thread entrera dans la file d'attente de verrouillage et sera suspendu par le système d'exploitation, en attendant d'être réveillé.
Au fil du temps et des threads en compétition, lorsque d'autres threads libèrent le verrou et que le système d'exploitation se rend compte qu'un thread attend le verrou, le système d'exploitation réveille le thread en attente, le redémarre et tente de réacquérir le verrou. Ce processus peut prendre un certain temps, après quoi le thread tente à nouveau d'acquérir le verrou pour poursuivre l'exécution.

Autres opérations d'optimisation

élimination du verrou

Le compilateur + JVM détermine si le verrou peut être éliminé, et si c'est le cas, il est éliminé directement.
En d’autres termes, lorsque bon nombre de nos opérations de verrouillage sont exécutées dans un seul thread, les verrous pour ces opérations de verrouillage ne sont pas nécessaires.

 @Override
    public synchronized StringBuffer append(String str) {
    
    
        toStringCache = null;
        super.append(str);
        return this;
    }

Par exemple, l'opération d'ajout dans StringBuffe impliquera une opération de verrouillage, et nous pouvons éliminer le verrou dans une opération monothread.

dégrossissage des serrures

Si plusieurs verrous et déverrouillages se produisent dans un élément de logique, le compilateur + la JVM rendront automatiquement les verrous plus grossiers.

L'exemple que nous avons utilisé en classe est le suivant :

Le leader assigne des tâches aux personnes ci-dessous. Il y a trois tâches au total. Il existe maintenant deux méthodes :

  1. Appelez un employé et effectuez trois tâches à la fois.
  2. Passez trois appels téléphoniques aux employés, une tâche à la fois.

Laissez-nous tous choisir, et tout le monde choisira certainement la méthode 1. Bien sûr, d'autres JVM effectueront également un tel grossissement des verrous.

Vous pouvez le comprendre avec un code :

        //频繁加锁
        for (int i = 0; i < 100; i++) {
    
    
            synchronized (o1){
    
    
            }
        }
        //粗化
        synchronized (o1){
    
    
            for (int i = 0; i < 100; i++) {
    
    
            }
        }

Grossissez le verrou pour éviter de demander et de libérer fréquemment le verrou.

Je suppose que tu aimes

Origine blog.csdn.net/st200112266/article/details/133085307
conseillé
Classement