ConcurrentHashMap du code source JAVA

Cet article a participé à l'événement "Newcomer Creation Ceremony" pour commencer ensemble la route de la création d'or.

Comme HashMap, la structure de données de ConcurrentHashMap dans les versions 1.7 et 1.8 est également différente.

La différence entre 1,7 et 1,8

1.7

ConcurrentHashMap dans JDK1.7 est composé d'une structure de tableau Segment et d'une structure de tableau HashEntry, c'est-à-dire que ConcurrentHashMap divise le tableau de compartiments de hachage en petits tableaux (Segments), et chaque petit tableau est composé de n HashEntry.

Comme illustré dans la figure ci-dessous, divisez d'abord les données en sections de stockage, puis affectez un verrou à chaque section de données. Lorsqu'un thread occupe le verrou pour accéder à une section de données, d'autres sections de données sont également accessibles par d'autres threads, réalisant le réel de l'accès simultané.

image-20220314141742909-16472386654097.pngSegment hérite de ReentrantLock, donc Segment est une sorte de verrou réentrant et joue le rôle de verrou. Le segment par défaut est 16, c'est-à-dire que la simultanéité est 16.

1.8

En termes de structure de données, ConcurrentHashMap dans JDK1.8 sélectionne le même tableau de nœuds + liste liée + structure arborescente rouge-noir que HashMap ; en termes d'implémentation de verrouillage, le verrouillage de segment d'origine est abandonné et CAS + synchronisé est utilisé pour obtenir plus mise en œuvre détaillée Verrouillage granulaire.

Le niveau de verrouillage est contrôlé à un niveau d'élément de tableau de hachage plus fin, c'est-à-dire que seul le nœud principal de la liste chaînée (le nœud racine de l' arbre rouge-noir ) doit être verrouillé, et il ne le sera pas. affecter d'autres éléments de tableau de compartiment de hachage. Lecture et écriture, améliorant considérablement la simultanéité.

image-20220314142026203.png

Pourquoi utiliser le verrou intégré synchronisé pour remplacer le verrou réentrant ReentrantLock dans JDK1.8 ?

  1. Dans JDK1.6, de nombreuses optimisations ont été introduites dans l'implémentation du verrouillage synchronisé, et synchronisé a plusieurs états de verrouillage, qui seront convertis étape par étape de pas de verrouillage -> verrouillage biaisé -> verrouillage léger -> verrouillage lourd.
  2. Réduisez la surcharge de mémoire. En supposant que des verrous réentrants sont utilisés pour la prise en charge de la synchronisation, chaque nœud doit hériter d'AQS pour la prise en charge de la synchronisation. Mais tous les nœuds n'ont pas besoin d'un support de synchronisation, seul le nœud principal de la liste chaînée (le nœud racine de l'arbre rouge-noir) doit être synchronisé, ce qui entraîne sans aucun doute un énorme gaspillage de mémoire.

Instructions de base

définition constante

//最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认容量
private static final int DEFAULT_CAPACITY = 16;
//最大的数组长度,toArray方法需要
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//负载因子
private static final float LOAD_FACTOR = 0.75f;
//链表树化的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树变成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表需要树化的最小容量要求
static final int MIN_TREEIFY_CAPACITY = 64;
//在进行扩容时单个线程处理的最小步长。
private static final int MIN_TRANSFER_STRIDE = 16;
//sizeCtl 中用于生成标记的位数。对于 32 位数组,必须至少为 6。
private static int RESIZE_STAMP_BITS = 16;
//可以帮助调整大小的最大线程数。必须适合 32 - RESIZE_STAMP_BITS 位。
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//在 sizeCtl 中记录大小标记的位移。
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//哈希表中的节点状态,会在节点的hash值中体现
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

//基本计数器值(拿来统计哈希表中元素个数的),主要在没有争用时使用,但也可作为表初始化竞赛期间的后备。通过 CAS 更新。
private transient volatile long baseCount;
//表初始化和调整大小控制。如果为负数,则表正在初始化或调整大小:-1 表示初始化,否则 -(1 + 活动调整大小线程的数量)。否则,当 table 为 null 时,保存要在创建时使用的初始表大小,或者默认为 0。初始化后,保存下一个元素计数值,根据该值调整表的大小。
private transient volatile int sizeCtl;
//调整大小时要拆分的下一个表索引(加一个)。
private transient volatile int transferIndex;
//调整大小和/或创建 CounterCell 时使用自旋锁(通过 CAS 锁定)。
private transient volatile int cellsBusy;
//计数单元表。当非空时,大小是 2 的幂。   与baseCount一起记录哈希表中的元素个数。
private transient volatile CounterCell[] counterCells;
复制代码

propagé

/**
将散列的较高位传播(XOR)到较低位,并将最高位强制为 0。由于该表使用二次幂掩码,因此仅在当前掩码之上的位中变化的散列集总是会发生冲突。 (已知的例子是在小表中保存连续整数的 Float 键集。)因此,我们应用了一种变换,将高位的影响向下传播。在位扩展的速度、实用性和质量之间存在折衷。因为许多常见的散列集已经合理分布(所以不要从传播中受益),并且因为我们使用树来处理 bin 中的大量冲突,我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算。
*/
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
复制代码

Après avoir traversé la propagation, la valeur de hachage de toutes les opérations de clé est un nombre supérieur ou égal à 0. Ainsi, ConcurrentHashMap utilise le hachage de Node pour enregistrer l'état du nœud. Reportez-vous aux définitions des constantes ci-dessus : MOVED, TREEBIN, RESERVED.

mettre l'opération

image-20220314180212299-16472521346978.png

initTable

L'opération d'initialisation est très simple, il suffit de regarder directement le code source

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
    //while循环一直来检查table是不是已经被初始化好了
        while ((tab = table) == null || tab.length == 0) {
            //看变量是不是小于0,负数表示有其他线程正在则表正在初始化或调整大小,当前线程像调度器表示当前线程愿意让出CPU
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // 失去了初始化竞赛;只是自旋
            //CAS加锁:比较当前对象的SIZECTL偏移量的位置的值是不是sc,如果是则将SIZECTL偏移量的位置的值设置成-1。    如果当前线程成功设置成-1,那么其他线程再CAS的时候就会发现这个地方的值不是原来的sc了,就加锁失败,退出。
            //另外:sc>0表示哈希表初始化或要扩容的大小
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //CAS成功后,再次判断table是不是未初始化,避免在CAS的之前一刻,其他线程完成了初始化操作。
                    if ((tab = table) == null || tab.length == 0) {
                        //计算扩容大小,如果没指定扩容大小,那么按默认容量初始化
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // >>> 无符号右移,所有sc=(3/4)n,也就是n*0.75;所以这行代码的意思就是把下一次扩容的阈值设置给sc
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //最后将sc设置给sizeCtl,try成功的情况下,sizeCtl记录的则是下一次扩容的阈值;
                    sizeCtl = sc;
                }
                //退出初始化操作
                break;
            }
        }
        return tab;
    }
复制代码

addCount

L'opération de transfert se produit lorsque l'expansion se produit, et l'expansion se produit après que le nombre d'éléments augmente :

Regardez la conception de comptage de ConcurrentHashmap avant de regarder le code source addCount, afin qu'il soit plus facile à comprendre lorsque vous regardez le code source.

Présentation de la conception de comptage

image-20220315144505540.png

logique de comptage

image-20220315144203536.png

procédure fullAddCount

TODO à ajouter

aideTransférer

TODO à ajouter

L'expansion et l'assistance à l'expansion sont assistées par la réception de tâches via plusieurs threads. L'expansion consiste à pointer d'abord la table suivante vers un tableau de nœuds de taille et de longueur étendues, puis à utiliser plusieurs threads pour faciliter le transfert des données de la table vers la table suivante. La tâche de transfert est reçue de la table par étapes selon la méthode de réception de la tâche. Une fois que le dernier thread a terminé la tâche de transfert, la table pointe vers la table suivante pour terminer l'opération d'expansion.

Je suppose que tu aimes

Origine juejin.im/post/7084931212871434271
conseillé
Classement