ConcurrentHashMap principe de l'analyse en profondeur

I. Contexte

Thread-safe HashMap

En raison de multiples environnements, pour le fonctionnement de mettre l'utilisation HashMap peut provoquer le cycle, ce qui entraîne de près de 100% d'utilisation du processeur, ne peut pas être utilisé dans le HashMap concurrency.

Inefficace récipient HashTable

récipient Hashtable en utilisant synchronisé pour assurer la sécurité du fil, mais dans l'efficacité la concurrence Hashtable très-thread très, très bas, parce que lorsque la méthode de synchronisation d'un fil pour accéder à la table de hachage, vous pouvez entrer dans l'état bloqué, tel que du fil 1 en utilisant des éléments add de vente, enfilez 2 ne sera pas en mesure d'ajouter des éléments mis à utiliser, et ne peut pas utiliser la méthode get pour obtenir les éléments, donc plus la concurrence, plus inefficace.

Segmentation verrouillage

récipient Hashtable manifeste dans un environnement très concurrentiel compliqué par cause sous - jacente de l' efficacité, parce que tous les fils de Hashtable d'accès doivent entrer en concurrence pour la même serrure, si elle verrouille le conteneur comment chaque partie des données pour le conteneur, puis lorsque plusieurs threads accéder au conteneur, il n'y a pas de compétition de verrouillage inter-thread. Qui peut effectivement améliorer l'efficacité de l' accès, ce qui est des techniques de segmentation de verrouillage ConcurrentHashMap, les premiers segments de données en stockant, pour chaque élément de données et d'une serrure, quand un fil de maintien du verrou dans lequel l'accès aux données, les autres segments de données peut également être consulté par d' autres threads, certaines méthodes ont besoin en coupe, tels que la taille () et ContainsValue (), ils peuvent verrouiller la table entière, plutôt qu'un verrou de segment, verrouiller tous les segments doivent à l' ordre, une fois l'opération terminée, et l'ordre libérer les verrous sur tous les segments. Voici très important dans l' ordre, ou est susceptible de blocage à l' intérieur ConcurrentHashMap, est le dernier segment de l'ensemble de données, et est en fait la variable membre finale, cependant, est que la déclaration finale du tableau ne membres de données non de garantie est définitive, ce qui est de faire en sorte que la nécessité d'atteindre, ce qui garantit qu'aucune impasse, parce que l'ordre pour obtenir le verrou fixe.
Insérer ici l'image Description
Structure de données de segment ConcurrentHashMap est un tableau de structures et HashEntry, un ReetrantLock de verrouillage de segment rentrant, le rôle ConcurrentHashMap dans la serrure, la HashEntry pour stocker des données clés, une matrice de ConcurrentHashMap qui contient un segment, le segment structure HashMap et analogues, une structure de liste et le réseau, qui contient un segment d' un réseau de HashEntry, chaque élément d'une liste est une structure HashEntry. Chaque segment HashEntry un réseau d'éléments de tuteur, lorsque le réseau de données est modifié HashEntry, doit d' abord obtenir son segment de verrouillage correspondant.

En deuxième lieu, les scénarios d'application

Quand il y a un large éventail au besoin lorsque la part de plusieurs threads peut envisager de le mettre en plusieurs noeuds, éviter de verrouillage grand. Examinons quelques-uns des modules et peut être positionné par l'algorithme de hachage. En fait, plus d'un fil, lorsque la table de données de conception des transactions (reflète aussi un sens mécanisme de synchronisation des affaires), l'instrument peut être considéré comme un tableau de synchronisation nécessaire, si la manipulation des données de la table est trop pour examiner les questions séparées ( c'est la raison pour laquelle vous voulez éviter une grande table), tels que les champs de données au niveau de répartition des sous-listes.

En troisième lieu, le code source d'interprétation

ConcurrentHashMap trois classes principales sont mises en œuvre, ConcurrentHashMap (toute la table de hachage), segment (barils), HashEntry (noeud), correspondant à la relation ci-dessus peut être vu entre le

/** 
* The segments, each of which is a specialized hash table 
*/  
final Segment<K,V>[] segments;

Constant (l'Immuable) et variable (volatile) `
de ConcurrentHashMap permettent totalement multiples exploitation simultanément, l'opération de lecture ne nécessite pas une serrure, en cas d' utilisation des techniques conventionnelles, comme mis en œuvre dans le HashMap, si possible Hash autorisé à ajouter ou supprimer la chaîne élément, une opération de lecture ne se verrouille pas les données incohérentes résultant. ConcurrentHashMap technologie de mise en œuvre est une garantie au nom de chaque nœud HashEntry chaîne Hash, structure qui est indiqué ci - dessous:

 static final class HashEntry<K,V> {  
     final K key;  
     final int hash;  
     volatile V value;  
     final HashEntry<K,V> next;  
 } 

En outre , vous pouvez voir, et non la valeur finale des autres valeurs sont définitives. Cela signifie que depuis le milieu ou la queue ne peut pas ajouter ou supprimer des chaînes de hachage nœuds, car il doit être modifié la valeur de référence suivante, tous les nœuds ne peuvent être modifiés de la tête, pour mettre toute l'opération peut ajouter la tête de hachage de la valeur de la liste, mais pour supprimer l' opération, vous devrez peut - être supprimer un nœud du milieu, qui est nécessaire pour supprimer la totalité du côté du noeud de réplication, les derniers points de nœud au nœud suivant à supprimer. Ceci sera décrit en détail quand on parle de la suppression, afin d'assurer que l'opération de lecture peut voir la dernière valeur, la valeur est définie à volatile, ce qui évite le blocage.
D' autres
afin d'accélérer la vitesse et le positionnement des segments de rainure de hachage du segment, la longueur de chaque fente est une table de hachage 2^n, ce qui rend la position de la section transversale et le positionnement de calcul de position de hachage fente de la boîte. Lorsque la valeur par défaut de 16 niveau de concurrence, ce qui est élevé 4 détermine le nombre de segments, la valeur de hachage alloué au segment, mais il ne faut pas oublier: le nombre de tranches de hachage ne doit pas être 2 ^ n, qui peut conduire à hachage rainure de répartition inégale, qui a besoin de la valeur re-hachage dans une table de hachage.
opération Positionnement:

 final Segment<K,V> segmentFor(int hash) {  
     return segments[(hash >>> segmentShift) & segmentMask];  
 }

Depuis ConcurrentHashMap l' utilisation de segmenter PROTECT segment de segment de données est différent, le temps d'insertion et d' obtenir les éléments, vous devez d' abord localiser grâce à un algorithme de hachage à segment, vous pouvez voir ConcurrentHashMap d' abord l' algorithme de hachage en utilisant une variété d'éléments encore une fois hashCode hash.
Re-hachage, son but est de réduire la collision de hachage, l'élément est uniformément répartie sur différents segments, ce qui améliore l'efficacité de l' accès du récipient, si le niveau de qualité de l' extrême de hachage pauvre. Donc , tous les éléments sont les mêmes dans un segment, non seulement des éléments d'accès lents, la zone de mise en scène quand il signifie. Je l' ai fait un test, non pas directement par hachage re-hachage.
System.out.println (le Integer.parseInt ( "0001111", 2) et 15);
System.out.println (la Integer.parseInt ( "0011111", 2) et 15);
System.out.println (la Integer.parseInt ( "0111111", 2) et 15);
System.out.println (le Integer.parseInt ( "1111111", 2) et 15), la
valeur de hachage sortie calculée 15 est pleine, par cet exemple peut être trouvé si plus hachage, collision de hachage sera très grave, car aussi longtemps aussi bas que, peu importe ce que le nombre est élevé, il est toujours la même valeur de hachage. Nous avons ensuite les données ci - dessus binaires après hachage résultat nouveau comme suit, pour faciliter la lecture, les 32 bits supérieurs représentent moins de 0, la barre verticale est divisée quatre.

0100 | 0111 | 0110 | 0111 | 1101 | 1010 | 0100 | 1110
1111 | 0111 | 0100 | 0011 | 0000 | 0001 | 1011 | 1000
0111 | 0111 | 0110 | 1001 | 0100 | 0110 | 0011 | 1110
1000 | 0011 | 0000 | 0000 | 1100 | 1000 | 0001 | 1010

Vous pouvez trouver tous les bits de données sont hachurées ouvert, et chacun dans cette re-hachage peut participer à faire numérique hachage eux, réduisant ainsi la collision de hachage. ConcurrentHashMap par segment de positionnement de hachage.
SegmentShift par défaut est 28, segmentMask 15, alors le nombre maximum de données binaire de hachage est de 32 bits, pas de symboles sont déplacés vers la 28 droite, les bits supérieurs 4 signifie que participer calcul de hachage (hash >>> segmentShift ) et résultat du calcul segmentMask 4,15,7 et 8 respectivement, on peut voir que la valeur de hachage ne sont pas en conflit.
Structure de données
Tous les membres sont finales, où segmentMask et segmentshfit principalement pour la section de positionnement, voir ci - dessus méthode segmentFor, sur la base de la structure de la table de hachage, mais le degré d'explorer ici, une table de hachage est un aspect important est de savoir comment résoudre le hachage des conflits et HashMap de ConcurrentHashMap de la même manière, qui est, de la même valeur de hachage du noeud dans une chaîne de hachage. Et hashmap sauf que, en utilisant une pluralité de sous ConcurrentHashMap table de hachage, le segment à -dire (segment).
Chaque sous-segment est tout à fait une table de hachage, ses membres de données sont les suivantes:

 /**
     * Stripped-down version of helper class used in previous version,
     * declared for the sake of serialization compatibility
     */
    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
         //loadFactor表示负载因子。
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

supprimer Supprimer (touche)

/**
     * {@inheritDoc}
     *
     * @throws NullPointerException if the specified key is null
     */
  public V remove(Object key) {  
   hash = hash(key.hashCode());   
   return segmentFor(hash).remove(key, hash, null);   
}

L'ensemble de l' opération est de localiser le segment, puis retirez confiée à la section de fonctionnement. Lorsque plusieurs opérations de suppression en même temps, tant qu'ils sont situés segments ne sont pas les mêmes, ils peuvent être effectués simultanément.
Voici la méthode de suppression de segment pour atteindre:

V remove(Object key, int hash, Object value) {  
     lock();  
     try {  
         int c = count - 1;  
         HashEntry<K,V>[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry<K,V> first = tab[index];  
         HashEntry<K,V> e = first;  
         while (e != null && (e.hash != hash || !key.equals(e.key)))  
             e = e.next;  
         V oldValue = null;  
         if (e != null) {  
             V v = e.value;  
             if (value == null || value.equals(v)) {  
                 oldValue = v;  

                 // All entries following removed node can stay  
                 // in list, but all preceding ones need to be  
                 // cloned.  
                 ++modCount;  
                 HashEntry<K,V> newFirst = e.next;  
                 *for (HashEntry<K,V> p = first; p != e; p = p.next)  
                     *newFirst = new HashEntry<K,V>(p.key, p.hash,  
                                                   newFirst, p.value);  
                 tab[index] = newFirst;  
                 count = c; // write-volatile  
             }  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

Toute l'opération est effectuée dans le cas d' une exploitation serrures de segment, les lignes vides avant que la ligne est principalement destiné à supprimer le noeud e. Ensuite, le nœud s'il n'y a pas de retour direct nul, sinon il est nécessaire de le copier à nouveau en face du nœud e, le point final au nœud suivant dans le nœud e. noeud e derrière la réplication n'est pas nécessaire, ils peuvent être réutilisés.
Le milieu de la boucle est ce qu'il faut faire avec elle? (Marqués avec *) à partir du code, est qu'après tout l'entrée et le clonage de position arrière vers l'avant pour se battre, mais nécessaire? Chaque élément est nécessaire d'enlever un élément avant que le clone à nouveau? Ceci est en fait l'entrée de l' invariance à la décision, les définitions d'entrée d' une observation attentive trouve en plus de valeur, tous les autres attributs sont utilisés pour modifier la finale, ce qui signifie qu'il ne peut plus être modifié après le premier jeu de domaine suivant et remplacer tout cela avant le clonage d' un nœud. Quant à savoir pourquoi l'entrée est définie sur invariance, qui ne nécessite pas la synchronisation avec l'invariance d'accès économisant ainsi du temps sur le
Voici un schéma de
Insérer ici l'image Description
Insérer ici l'image Description
 la deuxième figure est en fait un problème, la réplication du nœud doit être la valeur du nœud 2 à l' avant, la valeur 1 nœud est dans le dos, ce qui est exactement l'ordre inverse du nœud d' origine, mais heureusement, cela ne change pas notre discussion.
Supprimer est pas compliqué la mise en œuvre ensemble, mais nécessite une attention sur les points suivants. Tout d' abord, quand il y a un nœud à supprimer, la valeur Pour supprimer la dernière étape moins un comptage. Cela doit être la dernière étape de l'opération, ou l'opération de lecture ne peut pas voir les modifications structurelles apportées avant segment. En second lieu , supprimer commence l' exécution attribuera une table variable locale Tab, car la table variable est volatile, en lecture-écriture des variables volatiles surcharge importante. Le compilateur ne peut pas faire une lecture et d' écriture optimisation des variables volatiles, un accès direct à plusieurs variables exemple non volatiles a eu peu d' effet, le compilateur d' optimiser en conséquence.
obtenir un fonctionnement
ConcurrentHashMap l'opération GET est une méthode directe pour obtenir le segment de délégué, le segment de regarder directement la méthode get:

V get(Object key, int hash) {  
     if (count != 0) { // read-volatile 当前桶的数据个数是否为0 
         HashEntry<K,V> e = getFirst(hash);  得到头节点
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key)) {  
                 V v = e.value;  
                 if (v != null)  
                     return v;  
                     /**
                     *
                     /
                 return readValueUnderLock(e); // recheck  
             }  
             e = e.next;  
         }  
     }  
     returnnull;  
 } 
V readValueUnderLock(HashEntry<K,V> e) {  
     lock();  
     try {  
         return e.value;  
     } finally {  
         unlock();  
     }  
 }

Si elle trouve un désir de nœud, il est déterminé si la valeur de son retour non vide directement, ou de le lire à nouveau dans un état verrouillé. Il peut sembler difficile à comprendre, la valeur théorique du noeud ne peut être vide, car quand il a été mis pour déterminer si nous devrions jeter NullPointerException est vide. La seule source est la valeur par défaut des valeurs nulles dans HashEntry, parce que HashEntry la valeur est différente de lecture finale, non-synchrone est possible de lire à une valeur nulle. Regardez la déclaration d'opération soigneusement mis: onglet [index] = new HashEntry <K, V> (clé, hachage, première valeur), dans cette déclaration, constructeur de HashEntry et l'attribution de valeur de l'onglet [index] l'affectation peut être réorganisés, ce qui peut provoquer le nœud est vide. Ici, quand v est vide, il peut être un fil est noeud change, alors que l'opération précédente get aucune de la serrure, selon bernstein état, après la lecture d' écriture ou de lecture-écriture provoquera des données incohérentes, alors là encore sur cette e serrure lire à nouveau, ce qui garantit la valeur correcte.
opération de vente
même opération normale est chargée moyens mis ce segment, la méthode suivante est posée:

V put(K key, int hash, V value, boolean onlyIfAbsent) {  
     lock();  
     try {  
         int c = count;  
         if (c++ > threshold) // ensure capacity  
             rehash();  
         HashEntry<K,V>[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry<K,V> first = tab[index];  
         HashEntry<K,V> e = first;  
         while (e != null && (e.hash != hash || !key.equals(e.key)))  
             e = e.next;  
         V oldValue;  
         if (e != null) {  
             oldValue = e.value;  
             if (!onlyIfAbsent)  
                 e.value = value;  
         }  
         else {  
             oldValue = null;  
             ++modCount;  
             tab[index] = new HashEntry<K,V>(key, hash, first, value);  
             count = c; // write-volatile  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

Étant donné que la méthode était nécessaire pour mettre l'opération d'écriture variable partagée, de sorte que pour la sécurité des threads, doit être verrouillé dans la variable partagée de fonctionnement, la méthode du segment mis positionnement d'abord, puis fonctionner dans le segment. Les insertions doivent passer par deux étapes, détermine d'abord si oui ou non la nécessité pour le segment HashEntry dans l'expansion de la matrice, la seconde étape est ensuite positionné dans la position de l'élément ajouté à la HashEntry de réseau.

Le mode de fonctionnement containKey

//判断是否包含key
boolean containsKey(Object key, int hash) {  
     if (count != 0) { // read-volatile  
         HashEntry<K,V> e = getFirst(hash);  
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key))  
                 return true;  
             e = e.next;  
         }  
     }  
     returnfalse;  
 } 

taille () opération
si nous voulons être pris en compte dans la taille des éléments entiers de ConcurrentHashMap, il faut compter tous les segments de la taille de la somme des éléments, segment dans le nombre global variable est une variable volatile, dans les scénarios multithread, nous ne sommes pas directement la somme de tous les segments du comte , vous pouvez obtenir toute la taille de ConcurrentHashMap? Non, tout en ajoutant que l' accès à la dernière valeur de comptage du segment, mais la valeur de comptage avant les changements d' utilisation qui peuvent accumuler après guèt, le résultat est un permis statistique. Ainsi , l' approche de la sécurité est dans les statistiques lorsque la taille du segment de la vente, l' enlèvement, la méthode propre tout verrouillé, mais l'efficacité de cette approche est particulièrement faible.
Parce que la probabilité d'un changement dans le processus de l' opération de comptage de comptage accumulée, avant l'accumulation a eu un très petit, donc l' approche ConcurrentHashMap est de la première tentative de compter deux fois de ne pas bloquer la taille du segment de segment dans chaque sens, si le processus statistique, le récipient compte des changements, verrouillé, en utilisant à nouveau l'approche statistique de la taille de tous les segments.
Alors ConcurrentHashMap est de savoir comment déterminer si le navire a changé dans les statistiques quand il? Utiliser une variable modCount, l'élément avant variable modCount sera ajouté à 1, puis comparez modCount si les changements de taille avant et après les statistiques mises, supprimer et méthode propre en fonctionnement, de sorte que la taille du navire a changé.

Publié 21 articles originaux · a gagné les éloges 4 · Vues 513

Je suppose que tu aimes

Origine blog.csdn.net/weixin_39617728/article/details/104856416
conseillé
Classement