Analyse du code source du noyau ConcurrentHashMap

Titre de l'image

On ne doit s'oublier et aimer les autres que pour être tranquille, heureux et noble. -Tolstoy, Anna Karenina

0 Préface

Map-ConcurrentHashMap thread-safe, étudions quelle est la différence par rapport à HashMap, pourquoi pouvons-nous assurer la sécurité des threads.

1 Système de succession

Titre de l'image

Titre de l'image
Semblable à HashMap, la structure des tableaux et des listes liées est presque la même. Les deux implémentent l'interface Map et héritent de la classe abstraite AbstractMap. La plupart des méthodes sont également les mêmes. ConcurrentHashMap contient presque toutes les méthodes de HashMap.

2 Propriétés

  • Tableau bin. L'initialisation n'est retardée qu'après la première insertion. La taille est toujours une puissance de 2. Accès direct par l'itérateur.
    Titre de l'image
  • Le tableau suivant à utiliser; non nul uniquement lors de l'expansion
    Titre de l'image
  • Valeur de base du compteur, principalement utilisée lorsqu'il n'y a pas de conflit, et également utilisée comme rétroaction pendant la compétition d'initialisation de table.
    Titre de l'image
  • Si le contrôle de l'initialisation et de l'expansion de la table est négatif, la table sera initialisée ou développée: -1 est utilisé pour initialiser -N le nombre de threads d'expansion actifs. Sinon, lorsque la table est nulle, conservez la taille de table initiale à utiliser lors de la création, ou par défaut Il est égal à 0. Après l'initialisation, conservez la valeur du nombre d'éléments de la prochaine table d'extension.
    Titre de l'image
  • Index du prochain tableau à diviser lors de l'expansion (plus 1)
    Titre de l'image
  • Développer et / ou spin lock utilisé lors de la création de CounterCell (verrouillé via CAS)
    Titre de l'image
  • Tableau des contre-cellules. Si non nulle, la taille est une puissance de 2.
    Titre de l'image
  • Noeud: une structure de données qui contient des clés, des valeurs et des valeurs de hachage de clé, où la valeur et la suivante sont modifiées avec volatile pour assurer la visibilité
    Titre de l'image
  • Un nœud Node spécial, la valeur de hachage du nœud de transfert est MOVED, -1. Il stocke la référence de nextTable. Le nœud inséré dans la tête du bac pendant le transfert. ForwardingNode ne jouera un rôle d'espace réservé que lorsque la table sera développée. Le symbole est placé dans le tableau pour indiquer que le nœud actuel est nul ou a été déplacé,
    Titre de l'image

3 Méthode de construction

3.1 Aucun paramètre

  • Créer une nouvelle carte vide en utilisant la taille de table initiale par défaut (16)
    Titre de l'image

3.2 Participation

  • Créez une nouvelle carte vide dont la taille de table initiale peut accueillir le nombre spécifié d'éléments sans avoir à ajuster dynamiquement la taille.
    Titre de l'image
    -Créer une nouvelle carte avec la même cartographie que la carte donnée
    Titre de l'image

Notez que sizeCtl maintiendra temporairement la capacité d'une puissance de deux valeurs.

Lors de l'instanciation de ConcurrentHashMap avec des paramètres, la taille de la table sera ajustée en fonction des paramètres. En supposant que le paramètre est 100, il sera éventuellement ajusté à 256 pour garantir que la taille de la table est toujours une puissance de 2.

tableSizeFor

  • Pour une capacité requise donnée, renvoie la taille de la table en puissances de 2
    Titre de l'image

initialisation paresseuse de la table

ConcurrentHashMap initialise uniquement la valeur sizeCtl dans le constructeur, et n'initialise pas directement la table, mais retarde l'initialisation de la première table d'opérations de transfert. Mais put peut être exécuté simultanément, comment garantir que la table n'est initialisée qu'une seule fois?

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 进入自旋
    while ((tab = table) == null || tab.length == 0) {
        // 若某线程发现sizeCtl<0,意味着其他线程正在初始化,当前线程让出CPU时间片
        if ((sc = sizeCtl) < 0) 
            Thread.yield(); // 失去初始化的竞争机会; 直接自旋
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 有可能执行至此时,table 已经非空,所以做双重检验
                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 = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
复制代码

Le thread effectuant la première opération put exécutera la méthode Unsafe.compareAndSwapInt pour modifier sizeCtl à -1, et un seul thread peut être modifié avec succès, tandis que d'autres threads ne peuvent abandonner les tranches de temps CPU via Thread.yield () pour attendre la fin de l'initialisation de la table.

4 mettre

La table a été initialisée et l'opération put utilise la synchronisation CAS + pour implémenter des opérations d'insertion ou de mise à jour simultanées.

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 计算hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 自旋保证可以新增成功
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // step1. table 为 null或空时进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // step 2. 若当前数组索引无值,直接创建
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS 在索引 i 处创建新的节点,当索引 i 为 null 时,即能创建成功,结束循环,否则继续自旋
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // step3. 若当前桶为转移节点,表明该桶的点正在扩容,一直等待扩容完成
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // step4. 当前索引位置有值
        else {
            V oldVal = null;
            // 锁定当前槽点,保证只会有一个线程能对槽点进行修改
            synchronized (f) {
                // 这里再次判断 i 位置数据有无被修改
                // binCount 被赋值,说明走到了修改表的过程
                if (tabAt(tab, i) == f) {
                    // 链表
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 值有的话,直接返回
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 将新增的元素赋值到链表的最后,退出自旋
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
                    // TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 满足if的话,把老的值给oldVal
                        // 在putTreeVal方法里面,在给红黑树重新着色旋转的时候
                        // 会锁住红黑树的根节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // binCount不为空,并且 oldVal 有值的情况,说明已新增成功
            if (binCount != 0) {
                // 链表是否需要转化成红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                // 槽点已经上锁,只有在红黑树或者链表新增失败的时候
                // 才会走到这里,这两者新增都是自旋的,几乎不会失败
                break;
            }
        }
    }
    // step5. check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法扩容
    // 如果已经在扩容中了,check有无完成
    addCount(1L, binCount);
    return null;
}
复制代码

4.2 Processus d'exécution

  1. Si le tableau est vide, initialisez, après avoir terminé, passez à 2
  2. Calculer si le compartiment actuel a une valeur
    • Aucun, CAS est créé, continue à tourner après l'échec, jusqu'à ce qu'il réussisse
    • Oui, passez à 3
  3. Déterminer si le compartiment est un nœud de transfert (extension de capacité)
    • Oui, il tourne et attend que l'extension soit terminée, puis ajouté
    • Non, passez à 4
  4. Le compartiment a de la valeur, ajoutez un verrou de synchronisation au compartiment actuel
    • Liste liée, ajoutez des nœuds à la fin de la chaîne
    • Arbre rouge et noir, nouvelle méthode pour la version arbre rouge et noir
  5. Une fois l'ajout terminé, vérifiez si l'extension est requise

La mise en œuvre du verrouillage des trois axes via spin + CAS + synchronisation est très intelligente et nous fournit les meilleures pratiques pour la conception de code simultané!

5 transfert-expansion

À la fin de la méthode put pour vérifier si une expansion est requise, entrez la méthode de transfert à partir de la méthode addCount de la méthode put.

L'essentiel est de créer un nouveau tableau vide, puis de déplacer et de copier chaque élément dans le nouveau tableau.

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 旧数组的长度
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 如果新数组为空,初始化,大小为原数组的两倍,n << 1
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    // 新数组长度
    int nextn = nextTab.length;
    // 若原数组上是转移节点,说明该节点正在被扩容
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    // 自旋,i 值会从原数组的最大值递减到 0
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            // 结束循环的标志
            if (--i >= bound || finishing)
                advance = false;
            // 已经拷贝完成
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // 每次减少 i 的值
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        // if 任意条件满足说明拷贝结束了
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 拷贝结束,直接赋值,因为每次拷贝完一个节点,都在原数组上放转移节点,所以拷贝完成的节点的数据一定不会再发生变化
            // 原数组发现是转移节点,是不会操作的,会一直等待转移节点消失之后在进行操作
            // 也就是说数组节点一旦被标记为转移节点,是不会再发生任何变动的,所以不会有任何线程安全的问题
            // 所以此处直接赋值,没有任何问题。
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                // 节点的拷贝
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 如果节点只有单个数据,直接拷贝,如果是链表,循环多次组成链表拷贝
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 在新数组位置上放置拷贝的值
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 在老数组位置上放上 ForwardingNode 节点
                        // put 时,发现是 ForwardingNode 节点,就不会再动这个节点的数据了
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    // 红黑树的拷贝
                    else if (f instanceof TreeBin) {
                        // 红黑树的拷贝工作,同 HashMap 的内容,代码忽略
                        ...
                        // 在老数组位置上放上 ForwardingNode 节点
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}
复制代码

Processus d'exécution

  1. Copiez d'abord toutes les valeurs du tableau d'origine dans le nouveau tableau après expansion, première copie depuis la fin du tableau
  2. Lors de la copie des logements d'une baie, verrouillez d'abord les logements de la baie d'origine. Lors de la copie réussie vers une nouvelle baie, affectez les logements de la baie d'origine au nœud de transfert
  3. À ce stade, s'il existe de nouvelles données qui doivent être placées dans l'emplacement, il est constaté que l'emplacement est un nœud de transfert, et il attendra toujours, de sorte que les données correspondant à l'emplacement ne changeront pas tant que l'expansion n'est pas terminée.
  4. Copie de la fin de la baie vers la tête. Chaque fois que la copie réussit, les nœuds de la baie d'origine sont définis comme nœuds de transfert jusqu'à ce que toutes les données de la baie soient copiées dans la nouvelle baie. La totalité de la baie est directement affectée au conteneur de la baie et la copie est terminée.

6 Résumé

ConcurrentHashMap, en tant que carte simultanée, est un point nécessaire pour les entretiens et un conteneur simultané qui doit être maîtrisé dans le travail.

Je suppose que tu aimes

Origine juejin.im/post/5e934e215188256bdf72b691
conseillé
Classement