[Super détaillé] Exploration approfondie de la sécurité des threads en Java pour rendre votre programme plus fiable ~

Plongez dans la sécurité des threads en Java pour rendre vos programmes plus fiables !

Nous commencerons par les quatre questions suivantes et démêlerons les problèmes de multithreading de Java.

  1. Qu'est-ce que la sécurité des fils ?
  2. Comment atteindre la sécurité des threads ?
  3. Quelle est la différence entre les différentes approches de mise en œuvre de la sécurité des threads ?
  4. Comment atteindre la sécurité des threads HashMap ?

1. Qu'est-ce que la sécurité des threads ?


La sécurité des threads signifie que lorsque plusieurs threads accèdent simultanément à des ressources partagées, il n'y aura pas d'incohérence des données ou d'autres situations inattendues. Dans la programmation multithread, la sécurité des threads est très importante, car plusieurs threads peuvent accéder et modifier les mêmes données en même temps, s'ils ne sont pas correctement synchronisés, cela peut entraîner des problèmes tels que l'incohérence des données, les conditions de concurrence et les blocages.

Afin d'assurer la sécurité des threads, certaines technologies et méthodes doivent être utilisées pour assurer la cohérence et la synchronisation des données, telles que le mécanisme de verrouillage, le fonctionnement atomique, les variables locales de thread, etc. Les classes thread-safe couramment utilisées incluent Vector, CopyOnWriteArrayList, Hashtable, ConcurrentHashMap, les classes atomiques, etc.

2. Comment atteindre la sécurité des threads ?


En Java, la sécurité des threads peut être obtenue de plusieurs manières :

  1. mot-clé synchronisé : lu comme "Sen Ke Nai Ri De". Le mécanisme de verrouillage le plus basique de Java. L'utilisation du mot clé synchronized peut garantir une exclusion mutuelle lorsque plusieurs threads accèdent à des ressources partagées, garantissant qu'un seul thread peut accéder à des ressources partagées en même temps. Peut être utilisé dans des méthodes ou des blocs de code. Lorsqu'une méthode ou un bloc de code est modifié par le mot clé synchronized, un seul thread peut entrer dans la méthode ou le bloc de code, et les autres threads seront bloqués jusqu'à ce que le thread en cours ait fini de s'exécuter.

Le principe de mise en œuvre de synchronized : le bloc de code modifié par synchronized est appelé bloc de synchronisation. Lorsqu'un thread entre dans un bloc de synchronisation, il essaie d'acquérir le verrou de l'objet. Si l'objet n'est pas verrouillé ou si le verrou de l'objet a a été acquis, le compteur de verrou +1 ; Si l'objet a été verrouillé par d'autres threads, le thread entrera dans un état bloqué, attendant que d'autres threads libèrent le verrou. Lorsque d'autres threads libèrent le verrou, le thread en attente se réveille et réessaye d'acquérir le verrou et d'exécuter le code dans le bloc synchronisé. En même temps, un seul thread peut acquérir le verrou de l'objet et exécuter le code dans le bloc synchronisé.

En-tête d'objet : en Java, chaque objet a un en-tête d'objet (Object Header), qui est utilisé pour stocker les métadonnées de l'objet, y compris le code de hachage de l'objet (hashCode), l'état de verrouillage, l'état de la marque GC et d'autres informations. La taille de l'en-tête d'objet est fixe, occupant généralement 8 octets (système 64 bits) ou 4 octets (système 32 bits).
L'information la plus importante dans l'en-tête de l'objet est l'état du verrou, qui est utilisé pour implémenter le mécanisme de synchronisation du mot clé synchronized en Java. La valeur de l'état de verrouillage peut être aucun état de verrouillage, état de verrouillage biaisé, état de verrouillage léger ou état de verrouillage lourd. L'état de verrouillage dépend du conflit entre les threads et de la manière dont le verrou est utilisé.

Les méthodes suivantes sont utilisées pour le mot clé synchronized :

public class Counter {
    
    
    private int count;

    // synchronized 修饰方法
    public synchronized void increment() {
    
    
        count++;
    }

    // synchronized 修饰代码块
    public void add(int n) {
    
    
        synchronized (this) {
    
    
            count += n;
        }
    }
}

Points à noter lors de l'utilisation du mot clé synchronized :

  • La méthode est une méthode d'instance (pas une méthode statique)

Avantages : Facile à utiliser, prend en charge les serrures réutilisables.
Inconvénients : problèmes de performances, ne peut protéger que des blocs de code ou des méthodes.

  1. mot-clé volatil : lu comme « je suis trop européen ». Ne peut être utilisé que pour modifier des variables. L'utilisation du mot-clé volatile peut assurer la visibilité de la variable, même lorsque plusieurs threads accèdent à la même variable en même temps, la valeur de la variable est garantie d'être cohérente. De plus, volatile a également pour effet d'interdire le réarrangement des instructions. Lorsqu'une variable est modifiée par volatile, tous les threads accédant à la variable lisent la dernière valeur de la mémoire principale.

Visibilité : si deux threads modifient une variable volatile en même temps, puisque la variable volatile peut garantir la visibilité, leurs résultats de modification seront immédiatement rafraîchis dans la mémoire principale, afin qu'un autre thread puisse lire la dernière valeur.

L'ordre des opérations de lecture est le même que l'ordre des opérations d'écriture : si un thread lit une variable volatile et avant d'y écrire, un autre thread lit également la même variable volatile, puis après que le premier thread a écrit dans la variable , la valeur de la variable lue par un autre thread est la dernière valeur écrite par le premier thread, et non la valeur au moment de la lecture.

Réorganisation des instructions : afin d'optimiser l'efficacité de l'exécution du programme, le processeur ou le compilateur modifie l'ordre d'exécution des instructions sans modifier les résultats d'exécution du programme d'origine, de manière à réduire le temps d'attente d'exécution des instructions et à tirer parti du multi- niveau de performance du processeur Pipelining, réduction des erreurs de prédiction de branche, etc. Dans un environnement à thread unique, la réorganisation des instructions ne posera aucun problème, car le résultat final de l'exécution ne changera pas. Mais dans un environnement multi-thread, le réarrangement des instructions peut conduire à des résultats inattendus, tels que l'incohérence des données, le blocage, la boucle infinie et d'autres problèmes.

Les méthodes suivantes utilisent le mot clé volatile :

public class VolatileExample {
    
    
    private volatile int count = 0;

    public void increment() {
    
    
        count++;
    }

    public int getCount() {
    
    
        return count;
    }
}

Avantages : visibilité des variables sur tous les threads, lecture et écriture séquentielle
Inconvénients : l'atomicité ne peut être garantie, la lecture et l'écriture fréquentes de variables volatiles sont coûteuses

  1. Verrouiller le verrou : l'utilisation du mécanisme de verrouillage de verrouillage permet d'obtenir des opérations de verrouillage plus flexibles, ce qui est plus efficace que le mot-clé synchronisé. Le verrou de verrouillage le plus couramment utilisé est le verrou réentrant ReentrantLock, et Reentrant se prononce comme "Ryan porte spécial".

Une serrure rentrante signifie qu'une même serrure peut être verrouillée et déverrouillée plusieurs fois, chaque verrouillage doit correspondre à un déverrouillage, sinon la serrure sera toujours occupée. Le mot clé synchronized est également un verrou réentrant.

Les méthodes suivantes sont utilisées pour les verrous réentrants ReentrantLock :

import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    
    
    private ReentrantLock lock = new ReentrantLock();

    public void method1() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("method1");
            method2();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void method2() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("method2");
        } finally {
    
    
            lock.unlock();
        }
    }
}


Caractéristiques des serrures rentrantes :

  • Le verrouillage répété est pris en charge. Dans le même thread, un verrou réentrant peut verrouiller le même verrou plusieurs fois sans interblocage.

Verrouillage répété : il y a une variable (compteur de verrouillage) similaire à un compteur à l'intérieur. Lors du verrouillage, le compteur est +1, lors du déverrouillage, le compteur est -1, et le verrou est libéré lorsque le compteur est 0.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    
    
    private ReentrantLock lock = new ReentrantLock();

    public void foo() {
    
    
        lock.lock(); // 第一次加锁
        System.out.println(Thread.currentThread().getName() + " get lock.");
        lock.lock(); // 第二次加锁
        System.out.println(Thread.currentThread().getName() + " get lock again.");
        lock.unlock(); // 第一次释放锁
        System.out.println(Thread.currentThread().getName() + " release lock.");
        lock.unlock(); // 第二次释放锁
        System.out.println(Thread.currentThread().getName() + " release lock again.");
    }

    public static void main(String[] args) {
    
    
        ReentrantLockDemo demo = new ReentrantLockDemo();

        Thread t1 = new Thread(() -> {
    
    
            demo.foo();
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
    
    
            demo.foo();
        }, "Thread-2");

        t1.start();
        t2.start();
    }
}

//输出结果
//Thread-1 get lock.
//Thread-1 get lock again.
//Thread-1 release lock.
//Thread-1 release lock again.
//Thread-2 get lock.
//Thread-2 get lock again.
//Thread-2 release lock.
//Thread-2 release lock again.

  • Soutenez le verrouillage équitable et le verrouillage injuste. Un verrou réentrant peut spécifier s'il s'agit d'un verrou équitable ou d'un verrou injuste, et la valeur par défaut est un verrou injuste.

Verrouillage équitable et verrouillage injuste : le verrouillage équitable signifie que lorsque plusieurs threads attendent le verrou, ils acquièrent le verrou en séquence en fonction du temps d'attente, c'est-à-dire une stratégie du premier arrivé, premier servi. Les verrous injustes saisissent directement les verrous quel que soit le temps d'attente, ce qui peut empêcher certains threads d'obtenir les verrous.

  • La réponse d'interruption est prise en charge. Les verrous réentrants permettent de répondre aux interruptions en attendant le verrou.

  • Plusieurs variables de condition sont prises en charge. Les verrous réentrants peuvent créer une file d'attente pour chaque variable de condition et peuvent attendre ou réveiller un nombre spécifié de threads sur la variable de condition.

Avantages : les verrous peuvent être acquis de manière répétée pour éviter les interblocages, bonnes performances, évolutifs (verrous équitables, verrous injustes, verrous réentrants en lecture-écriture, etc.) Inconvénients :
code complexe

  1. Opération atomique : l'opération atomique est une méthode d'opération thread-safe sans verrouillage, qui peut assurer la cohérence des données lorsque plusieurs threads accèdent à la même variable en même temps. En Java, la sécurité des threads est obtenue en utilisant la méthode d'opération atomique dans la classe d'opération atomique.

Avantages : Garantir l'intégrité de l'opération, pas besoin de verrouiller, assurer la cohérence et la visibilité des données
Inconvénients : Ne peut pas garantir l'ordre des accès concurrents, ne peut pas garantir l'atomicité des données (si les données d'opération sont relativement volumineuses, il faut quand même être verrouillé pour assurer l'Atomicité), la mise en œuvre est plus compliquée

  1. Classes de collection thread-safe : Java fournit certaines classes de collection thread-safe, telles que Vector, CopyOnWriteArrayList, Hashtable, ConcurrentHashMap, etc.

Vecteur : il peut être compris comme une ArrayList thread-safe. La méthode fournie est similaire à celle de ArrayList, et elle est implémentée à l'aide du mot clé synchronized. Il est relativement ancien et peut être comparé à la relation entre HashTable et HashMap.
CopyOnWriteArrayList : thread-safe ArrayList, copiez le tableau d'origine, puis modifiez le nouveau tableau et enfin affectez-le à la référence de tableau d'origine. Il est plus efficace que Vector.
Classes d'opérations atomiques : 7 types, dont AtomicInteger, AtomicLong et AtomicBoolean.

Il est nécessaire de sélectionner la technologie et la méthode de sécurité des threads appropriées en fonction de la situation spécifique pour garantir que plusieurs threads peuvent accéder aux ressources partagées correctement et efficacement.

3. Quelles sont les différences entre les différentes implémentations de thread safety ?


Différentes méthodes de mise en œuvre de la sécurité des threads ont des scénarios et des performances applicables différents. Par exemple, le mot clé synchronized convient pour verrouiller des sections critiques, ce qui peut garantir la sécurité des threads, mais les performances peuvent être affectées ; tandis que l'utilisation de classes de collection thread-safe telles que ConcurrentHashMap peut améliorer les performances et les performances simultanées dans des situations de concurrence élevée.

granularité performance Facilité d'utilisation
synchronisé Méthode modifiée ou bloc de code, l'objet est la classe entière ou la méthode entière, une variable membre ou un bloc de code dans la classe Performances médiocres, ne convient pas aux scénarios à forte simultanéité. Ajoutez simplement le mot clé synchronized avant la méthode ou le bloc de code qui doit être synchronisé
volatil Variable modifiée, l'objet de l'action est la variable Frais généraux élevés lors de la lecture et de l'écriture fréquentes Ajoutez simplement le mot-clé volatile avant la variable
Serrure réentrante ReentrantLock L'objet de l'action est une variable ou un bloc de code Il offre de bonnes performances et convient aux scénarios à forte simultanéité. Vous devez verrouiller et déverrouiller manuellement le verrou vous-même
opération atomique L'objet de l'action est une variable ou un bloc de code relativement faible La mise en œuvre est relativement compliquée et nécessite une compréhension approfondie de la plate-forme matérielle et du système d'exploitation, et a des exigences relativement élevées pour les développeurs

4. Comment atteindre la sécurité des threads HashMap ?


  1. HashMap et Hashtable
la différence HashMap Table de hachage
sécurité des fils Non sécurisé, nécessite une synchronisation supplémentaire Sécurité
valeur nulle La clé et la valeur peuvent être nulles interdit
Capacité initiale et mécanisme d'expansion La capacité initiale par défaut est de 16 et le mécanisme d'expansion consiste à doubler la longueur du tableau lorsque le nombre d'éléments est supérieur au produit du facteur de charge et de la longueur du tableau La capacité initiale par défaut est 11. Le mécanisme d'expansion consiste à doubler la longueur du tableau et à ajouter 1 lorsque le nombre d'éléments est supérieur à la longueur du tableau.
mode de traversée Réalisé par Iterator Réalisé par énumération
  1. Foire aux questions sur HashTable :
    HashTable est thread-safe mais HashMap n'est pas thread-safe : Hashtable utilise un mécanisme de synchronisation pour assurer la sécurité des threads, c'est-à-dire que le mot clé synchronized est utilisé sur chaque méthode publique pour s'assurer qu'un seul thread fonctionne Hashtable à un temps. HashMap n'utilise pas ce mécanisme de synchronisation.

Code source de la méthode publique HashTable :

// 判断Hashtable中是否存在某个value
public synchronized boolean contains(Object value) {
    
    
    if (value == null) {
    
    
        throw new NullPointerException();
    }

    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
    
    
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
    
    
            if (e.value.equals(value)) {
    
    
                return true;
            }
        }
    }
    return false;
}

La clé et la valeur de HashTable ne peuvent pas être nulles : pour Hashtable, lors de l'insertion d'un élément, s'il existe déjà un élément dans le compartiment, utilisez la méthode equals pour comparer si la clé nouvellement insérée est égale à la clé existante dans le compartiment. compartiment. Lors de la comparaison si la clé est égale La méthode equals de la clé doit être appelée. Si la clé est null, il n'y a pas de méthode equals. La valeur de Hashtable ne peut pas être nulle, car dans Hashtable, la valeur est stockée dans un tableau de type Object, et chaque élément du tableau est un objet de valeur distinct, et non une référence à une valeur. Par conséquent, autoriser la valeur nulle entraînerait l'impossibilité de faire la distinction entre les emplacements vides dans le tableau et les emplacements qui stockent en fait des valeurs nulles. Cela peut entraîner des résultats inattendus lors de l'utilisation de Hashtable. Pour éviter cela, Hashtable n'autorise pas les valeurs nulles.

La clé et la valeur de HashMap autorisent null : l'objectif de conception de HashMap est de fournir autant que possible des opérations de recherche, d'insertion et de suppression efficaces, il n'y a donc aucune restriction sur le type de valeur. Cela permet aux utilisateurs d'utiliser librement null comme valeur en cas de besoin, ce qui augmente la flexibilité. Dans HashMap, si la clé est nulle, sa valeur de hachage est 0, elle sera donc placée en 0ème position de la table de hachage.

  1. Implémenter la sécurité des threads de HashMap
  • Utilisez ConcurrentHashMap
    qui est une implémentation HashMap thread-safe fournie par Java. Il implémente la sécurité des threads via des verrous de segment (Segment), et plusieurs threads peuvent accéder à différents segments en même temps, améliorant ainsi la concurrence.

La différence entre ConcurrentHashMap et HashTable : Hashtable est basé sur la synchronisation pour assurer la sécurité des threads, tandis que ConcurrentHashMap utilise des verrous segmentés pour assurer la sécurité des threads. Si vous devez utiliser une table de hachage dans un environnement multithread, il est recommandé d'utiliser ConcurrentHashMap.

  • Utilisez Collections.synchronizedMap
    , qui est une classe d'outils fournie par Java pour encapsuler une Map non-thread-safe dans une Map thread-safe. Il assure la sécurité des threads en ajoutant des verrous de synchronisation (à l'aide du mot clé synchronized) aux opérations Map.

Exemple de code thread-safe utilisant Collections.synchronizedMap :

Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

synchronizedMap.put("key1", "value1");
synchronizedMap.put("key2", "value2");

String value = synchronizedMap.get("key1");
System.out.println(value);

  • Utilisation du mécanisme de verrouillage
    Vous pouvez implémenter vous-même le mécanisme de verrouillage pour garantir la sécurité des threads de HashMap. Par exemple, vous pouvez utiliser le mot-clé synchronized ou ReentrantLock pour implémenter le mécanisme de verrouillage afin de garantir qu'un seul thread accède au HashMap en même temps.

Dans un environnement multithread, il est recommandé d'utiliser ConcurrentHashMap car il offre une concurrence plus élevée et de meilleures performances, mais vous devez faire attention à certains détails, tels que la nécessité d'utiliser des itérateurs lors de la traversée. Et s'il s'agit d'une simple exigence de sécurité des threads, vous pouvez envisager d'utiliser Collections.synchronizedMap.

Guess you like

Origin blog.csdn.net/m0_56170277/article/details/129786479