Explication détaillée des conteneurs de collecte en Java : utilisation simple et analyse de cas

Table des matières

1. Vue d'ensemble

1.1 Collecte

1 jeu

2. Liste

3. File d'attente

1.2 Carte

2. Modèles de conception dans des conteneurs 

modèle d'itérateur

mode adaptateur

3. Analyse du code source

Liste des tableaux

1. Vue d'ensemble

2. Expansion

3. Supprimer des éléments

4. Sérialisation

5. Échec rapide

Vecteur

1. Synchronisation

2. Expansion

3. Comparaison avec ArrayList

4. Alternatives

CopyOnWriteArrayList

1. Séparation de la lecture et de l'écriture

2. Scénarios applicables

Liste liée

1. Vue d'ensemble

2. Comparaison avec ArrayList

Carte de hachage

1. Structure de stockage

2. Comment fonctionne la méthode de la fermeture éclair

3. mettre l'opération

4. Déterminez l'indice du compartiment

5. Principes de base de l’expansion des capacités

6. Comparaison avec Hashtable

7. Les objets sont stockés sous forme de clés

ConcurrentHashMap

1. Structure de stockage

2. opération de taille

3. Modifications du JDK 1.8

LinkedHashMap

structure de stockage

aprèsNodeAccess()

aprèsNodeInsertion()


       Les conteneurs Java sont un ensemble d'outils permettant de stocker des données et des objets. Il peut être comparé au STL de C++. Les conteneurs Java sont également appelés Java Collection Framework (JCF). En plus des conteneurs qui stockent les objets, un ensemble de classes utilitaires est fourni pour traiter et manipuler les objets dans les conteneurs. De manière générale, il s'agit d'un framework qui contient des conteneurs d'objets Java et des classes utilitaires.

1. Vue d'ensemble

          Les conteneurs incluent principalement Collection et Map . Collection stocke une collection d'objets, tandis que Map stocke une table de mappage de paires clé-valeur (deux objets).

1.1 Collecte

1 jeu
  • TreeSet : basé sur l'implémentation d'un arbre rouge-noir, prend en charge les opérations ordonnées, telles que la recherche d'éléments en fonction d'une plage. Cependant, l'efficacité de la recherche n'est pas aussi bonne que celle de HashSet. La complexité temporelle de la recherche de HashSet est O(1), tandis que celle de TreeSet est O(logN).
  • HashSet : basé sur l'implémentation de la table de hachage, prend en charge la recherche rapide, mais ne prend pas en charge les opérations ordonnées. Et les informations sur l'ordre d'insertion des éléments sont perdues, ce qui signifie que le résultat obtenu en utilisant Iterator pour parcourir le HashSet est incertain.
  • LinkedHashSet : il a l'efficacité de recherche de HashSet et utilise en interne une liste doublement chaînée pour maintenir l'ordre d'insertion des éléments.
2. Liste
  • ArrayList : basé sur l'implémentation d'un tableau dynamique, prend en charge l'accès aléatoire.
  • Vector : similaire à ArrayList, mais il est thread-safe.
  • LinkedList : Basé sur une liste doublement chaînée, elle n'est accessible que de manière séquentielle, mais elle permet d'insérer et de supprimer rapidement des éléments au milieu de la liste chaînée. De plus, LinkedList peut également être utilisé comme pile, file d'attente et deque.
3. File d'attente
  • LinkedList : Vous pouvez l'utiliser pour implémenter une file d'attente bidirectionnelle.
  • PriorityQueue : Basé sur la structure du tas, vous pouvez l'utiliser pour implémenter des files d'attente prioritaires.

1.2 Carte

  • TreeMap : implémenté sur la base d'arbres rouge-noir.
  • HashMap : basé sur l'implémentation de la table de hachage.
  • HashTable : similaire à HashMap, mais il est thread-safe, ce qui signifie que plusieurs threads écrivant sur HashTable en même temps ne provoqueront pas d'incohérence des données. Il s'agit d'une classe héritée qui ne doit pas être utilisée. Utilisez plutôt ConcurrentHashMap pour prendre en charge la sécurité des threads. ConcurrentHashMap sera plus efficace car ConcurrentHashMap introduit des verrous de segmentation.
  • LinkedHashMap : utilisez une liste doublement chaînée pour conserver l'ordre des éléments dans l'ordre d'insertion ou dans l'ordre des moins récemment utilisés (LRU).

2. Modèles de conception dans des conteneurs 

modèle d'itérateur

Collection hérite de l'interface Iterable, dans laquelle la méthode iterator() peut générer un objet Iterator, à travers lequel les éléments de la Collection peuvent être itérés.

À partir du JDK 1.5, vous pouvez utiliser la méthode foreach pour parcourir les objets agrégés qui implémentent l'interface Iterable.

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
    System.out.println(item);
}

mode adaptateur

java.util.Arrays#asList() peut convertir le type de tableau en type de liste.

@SafeVarargs
public static <T> List<T> asList(T... a)

Il convient de noter que les paramètres de asList() sont des paramètres génériques de longueur variable. Les tableaux de types de base ne peuvent pas être utilisés comme paramètres et seuls les tableaux de types d'empaquetage correspondants peuvent être utilisés.

Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);

asList() peut également être appelé en utilisant :

List list = Arrays.asList(1, 2, 3);

3. Analyse du code source

Sauf indication contraire, l'analyse du code source suivante est basée sur JDK 1.8.

Dans IDEA, utilisez Double Shift pour appeler Search EveryWhere, recherchez les fichiers de code source, puis lisez le code source.

Liste des tableaux

1. Vue d'ensemble

ArrayList étant implémenté sur la base de tableaux, il prend en charge un accès aléatoire rapide. L'interface RandomAccess indique que la classe prend en charge l'accès aléatoire rapide.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

La taille par défaut du tableau est 10.

private static final int DEFAULT_CAPACITY = 10;
2. Expansion

Lors de l'ajout d'éléments, utilisez la méthode EnsureCapacityInternal() pour vous assurer que la capacité est suffisante. Si elle ne suffit pas, vous devez utiliser la méthode grow() pour augmenter la capacité. La taille de la nouvelle capacité est oldCapacity + oldCapacity oldCapacity + (oldCapacity >> 1)/ 2 . Parmi eux, oldCapacity >> 1 doit être arrondi, donc la nouvelle capacité est environ 1,5 fois l'ancienne capacité. (Si oldCapacity est un nombre pair, il est 1,5 fois, et s'il s'agit d'un nombre impair, il est 1,5 fois-0,5)

L'opération d'expansion nécessite un appel pour copier l'intégralité du tableau d'origine dans le nouveau tableau. Cette opération est très coûteuse, il est donc préférable de spécifier la capacité approximative lors de la création de l'objet ArrayList afin de réduire le nombre d'opérations d'expansion.Arrays.copyOf()

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
3. Supprimer des éléments

Vous devez appeler System.arraycopy() pour copier tous les éléments après index+1 vers la position d'index. La complexité temporelle de cette opération est O(N) . Vous pouvez voir que le coût de suppression d'éléments dans ArrayList est très élevé.

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; 
    return oldValue;
}
4. Sérialisation

ArrayList est implémenté sur la base de tableaux et possède des caractéristiques d'expansion dynamique. Par conséquent, les tableaux stockant les éléments ne peuvent pas tous être utilisés, il n'est donc pas nécessaire de tous les sérialiser.

Le tableau elementData qui contient les éléments est modifié avec transient. Ce mot clé déclare que le tableau ne sera pas sérialisé par défaut.

transient Object[] elementData;

ArrayList implémente writeObject() et readObject() pour contrôler la sérialisation uniquement de la partie du tableau remplie d'éléments.

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
    s.defaultReadObject();
    s.readInt(); 
    if (size > 0) {
        ensureCapacityInternal(size);
        Object[] a = elementData;
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(size);
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

Lors de la sérialisation, vous devez utiliser writeObject() de ObjectOutputStream pour convertir l'objet en flux d'octets et le générer. La méthode writeObject() reflétera et appellera writeObject() de l'objet lorsque l'objet entrant existe dans writeObject() pour réaliser la sérialisation. La désérialisation utilise la méthode readObject() d'ObjectInputStream, et le principe est similaire.

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
5. Échec rapide
Le mécanisme Fail-Fast est un mécanisme d'erreur dans les collections Java . Lorsque plusieurs threads effectuent des opérations sur le contenu de la même collection
Pendant le fonctionnement, des événements de défaillance rapide peuvent survenir .
Par exemple : lorsqu'un thread A parcourt une collection via un itérateur , si le contenu de la collection est modifié par d'autres threads
, puis lorsque le thread A accède à la collection, une exception ConcurrentModificationException sera levée, entraînant un événement fail-fast .
pièces. Les opérations ici font principalement référence à add , delete et clear , qui modifient le nombre d'éléments de la collection.
Solution : Il est recommandé d'utiliser les " classes sous le package java.util.concurrent " pour remplacer les " classes sous le package java.util " .
Cela peut être compris de cette façon : avant de parcourir, notez modCount et expectModCount , puis accédez à expectModCount .
Comparez avec modCount. S'ils ne sont pas égaux, cela prouve qu'il a été concurrent et modifié, donc il lance
Exception ConcurrentModificationException (exception de modification simultanée)

Vecteur

1. Synchronisation

Son implémentation est similaire à ArrayList, mais utilise synchronisé pour la synchronisation.

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
}
2. Expansion

Le constructeur de Vector peut transmettre le paramètrecapacitéIncrement, qui est utilisé pour augmenter la capacité decapacitéIncrement lors de l'expansion. Si la valeur de ce paramètre est inférieure ou égale à 0, la capacité sera doublée à chaque fois lors de l'expansion.

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}




private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}


//调用没有 capacityIncrement 的构造函数时,
capacityIncrement 值被设置为 0,也就是说默认情况下 Vector 每次扩容时容量都会翻倍。


public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
public Vector() {
    this(10);
}




3. Comparaison avec ArrayList
  • Vector est synchronisé, donc la surcharge est supérieure à ArrayList et la vitesse d'accès est plus lente. Il est préférable d'utiliser ArrayList au lieu de Vector, car les opérations de synchronisation peuvent être entièrement contrôlées par le programmeur lui-même ;
  • Vector nécessite 2 fois sa taille à chaque fois qu'il est étendu (la capacité croissante peut également être définie via le constructeur), tandis qu'ArrayList en nécessite 1,5 fois.
4. Alternatives

Vous pouvez utiliser Collections.synchronizedList();pour obtenir une ArrayList thread-safe.

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

Vous pouvez également utiliser la classe CopyOnWriteArrayList sous le package concurrent .

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

1. Séparation de la lecture et de l'écriture

L'opération d'écriture est effectuée sur un tableau copié, et l'opération de lecture est toujours effectuée sur le tableau d'origine. La lecture et l'écriture sont séparées et ne s'affectent pas.

Les opérations d'écriture doivent être verrouillées pour éviter la perte de données écrites en raison d'écritures simultanées.

Une fois l'opération d'écriture terminée, le tableau d'origine doit pointer vers le nouveau tableau copié.

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}
2. Scénarios applicables

CopyOnWriteArrayList permet des opérations de lecture en même temps que des opérations d'écriture, ce qui améliore considérablement les performances des opérations de lecture, il est donc très approprié pour les scénarios d'application avec plus de lecture et moins d'écriture.

Mais CopyOnWriteArrayList a ses défauts :

  • Utilisation de la mémoire : lors de l'écriture, un nouveau tableau doit être copié, ce qui entraîne une utilisation de la mémoire environ deux fois supérieure à la taille d'origine ;
  • Incohérence des données : l'opération de lecture ne peut pas lire les données en temps réel car certaines données de l'opération d'écriture n'ont pas encore été synchronisées avec le groupe de lecture.

Par conséquent, CopyOnWriteArrayList ne convient pas aux scénarios d’exigences sensibles à la mémoire et en temps réel.

Liste liée

1. Vue d'ensemble

Basé sur l'implémentation d'une liste doublement chaînée, Node est utilisé pour stocker les informations sur les nœuds de liste chaînée.

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}

每个链表存储了 first 和 last 指针:

transient Node<E> first;
transient Node<E> last;
2. Comparaison avec ArrayList

ArrayList est implémenté sur la base de tableaux dynamiques et LinkedList est implémenté sur la base de listes doublement liées. La différence entre ArrayList et LinkedList peut être attribuée à la différence entre les tableaux et les listes chaînées :

  • Les tableaux prennent en charge l'accès aléatoire, mais l'insertion et la suppression sont coûteuses et nécessitent le déplacement d'un grand nombre d'éléments ;
  • Les listes chaînées ne prennent pas en charge l'accès aléatoire, mais l'insertion et la suppression nécessitent uniquement de changer le pointeur.

Carte de hachage

Afin de faciliter la compréhension, l'analyse du code source suivante est principalement basée sur le JDK 1.7.

1. Structure de stockage

Il contient une table matricielle de type Entry en interne. Entry stocke les paires clé-valeur. Il contient quatre champs. Dans le champ suivant, nous pouvons voir que Entry est une liste chaînée. Autrement dit, chaque position du tableau est considérée comme un compartiment et chaque compartiment stocke une liste chaînée. HashMap utilise la méthode zipper pour résoudre les conflits. Les entrées avec la même valeur de hachage et le même résultat de l'opération modulo du compartiment de hachage sont stockées dans la même liste chaînée.

transient Entry[] table;


static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }
}
2. Comment fonctionne la méthode de la fermeture éclair
HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
  • Créez un nouveau HashMap, la taille par défaut est 16 ;
  • Insérez la paire clé-valeur <K1, V1>, calculez d'abord le hashCode de K1 à 115 et utilisez la méthode division-leaving-remainder pour obtenir l'indice du compartiment 115%16=3.
  • Insérez la paire clé-valeur <K2, V2>, calculez d'abord le hashCode de K2 à 118 et utilisez la méthode division-leaving-remainder pour obtenir l'indice du compartiment 118%16=6.
  • Insérez la paire clé-valeur <K3,V3>, calculez d'abord le hashCode de K3 à 118, utilisez la méthode de division et de reste pour obtenir l'indice du compartiment 118%16=6 et insérez-le devant <K2,V2>.

A noter que l'insertion de la liste chaînée s'effectue par insertion de tête. Par exemple, le <K3, V3> ci-dessus n'est pas inséré après <K2, V2>, mais en tête de la liste chaînée.

La recherche doit être divisée en deux étapes :

  • Calculez le compartiment où se trouve la paire clé-valeur ;
  • Lors d'une recherche séquentielle sur une liste chaînée, la complexité temporelle est évidemment proportionnelle à la longueur de la liste chaînée.
3. mettre l'opération
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 键为 null 单独处理
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    // 确定桶下标
    int i = indexFor(hash, table.length);
    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 插入新键值对
    addEntry(hash, key, value, i);
    return null;
}

 HashMap permet d'insérer des paires clé-valeur avec des clés nulles. Cependant, comme la méthode hashCode() de null ne peut pas être appelée, l'index de compartiment de la paire clé-valeur ne peut pas être déterminé et il ne peut être stocké qu'en spécifiant de force un index de compartiment. HashMap utilise le 0ème compartiment pour stocker les paires clé-valeur avec des clés nulles.

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

Utilisez la méthode d'insertion de tête de la liste chaînée, c'est-à-dire que la nouvelle paire clé-valeur est insérée en tête de la liste chaînée, et non à la queue de la liste chaînée.

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 头插法,链表头部指向新的键值对
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}




Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
4. Déterminez l'indice du compartiment

De nombreuses opérations nécessitent d'abord de déterminer l'index de compartiment où se trouve une paire clé-valeur.

int hash = hash(key);
int i = indexFor(hash, table.length);

4.1 Calculer la valeur de hachage

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}


public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

4.2 Modélisation

Soit x = 1<<4, c'est-à-dire que x est la 4ème puissance de 2, qui a les propriétés suivantes :

x   : 00010000
x-1 : 00001111



令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
y       : 10110010
x-1     : 00001111
y&(x-1) : 00000010


这个性质和 y 对 x 取模效果是一样的:

y   : 10110010
x   : 00010000
y%x : 00000010


我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运

static int indexFor(int h, int length) {
    return h & (length-1);
}
5. Principes de base de l’expansion des capacités

Supposons que la longueur de la table de HashMap est M et que le nombre de paires clé-valeur qui doivent être stockées est N. Si la fonction de hachage répond aux exigences d'uniformité, alors la longueur de chaque liste chaînée est d'environ N/M, donc la recherche la complexité est O(N/M).

Afin de réduire le coût de recherche, N/M doit être aussi petit que possible, donc M doit être aussi grand que possible, c'est-à-dire que le tableau doit être aussi grand que possible. HashMap utilise une expansion dynamique pour ajuster la valeur M en fonction de la valeur N actuelle, afin que l'efficacité spatiale et l'efficacité temporelle puissent être garanties.

Les paramètres liés à l'expansion incluent principalement : la capacité, la taille, le seuil et le facteur de charge.

paramètre signification
capacité La capacité de la table, la valeur par défaut est 16. Il convient de noter que la capacité doit être garantie égale à 2 à la puissance n.
taille Nombre de paires clé-valeur.
seuil La valeur critique de la taille. Lorsque la taille est supérieure ou égale au seuil, une opération d'expansion doit être effectuée.
facteur de charge Facteur de charge, la proportion que la table peut utiliser, seuil = (int)(capacité* loadFactor).
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;

 Comme le montre le code d'ajout d'éléments ci-dessous, lorsqu'une extension est nécessaire, la capacité est doublée.

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

L'expansion est implémentée à l'aide de resize(). Il convient de noter que l'opération d'expansion nécessite également de réinsérer toutes les paires clé-valeur de oldTable dans la newTable, cette étape prend donc beaucoup de temps.

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
6. Comparaison avec Hashtable
  • Hashtable utilise synchronisé pour la synchronisation.
  • HashMap peut insérer des entrées avec des clés nulles.
  • L'itérateur de HashMap est un itérateur à échec rapide.
  • HashMap ne peut pas garantir que l'ordre des éléments dans la carte restera inchangé dans le temps.
7. Les objets sont stockés sous forme de clés
  • Remplacez les méthodes hashCode() et equals() pour garantir que la carte peut fonctionner et récupérer correctement les objets.
  • Garantissez l’immuabilité de l’objet pour éviter de modifier l’état de l’objet après son utilisation comme clé.
  • Implémente éventuellement l'interface Comparable pour prendre en charge le tri des clés.
  • Méthode hashCode() bien conçue pour réduire le risque de collisions de hachage.
  • Évitez d'utiliser des objets mutables comme clés et mettez à jour les clés dans la carte rapidement si nécessaire.
     

ConcurrentHashMap

1. Structure de stockage
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}

ConcurrentHashMap et HashMap sont similaires dans leur implémentation. La principale différence est que ConcurrentHashMap utilise des verrous de segment (Segment). Chaque verrou de segment maintient plusieurs compartiments (HashEntry). Plusieurs threads peuvent accéder simultanément aux compartiments sur différents verrous de segment. Cela rend la concurrence plus élevée. (la concurrence est le nombre de segments).

Le segment hérite de ReentrantLock.

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
    transient volatile HashEntry<K,V>[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
}


final Segment<K,V>[] segments;

默认的并发级别为 16,也就是说默认创建 16 个 Segment。

static final int DEFAULT_CONCURRENCY_LEVEL = 16;
2. opération de taille

Chaque segment conserve une variable de comptage pour compter le nombre de paires clé-valeur dans le segment.

transient int count;

Lors de l'exécution de l'opération de taille, il est nécessaire de parcourir tous les segments puis d'accumuler le décompte.

ConcurrentHashMap essaie d'abord de ne pas se verrouiller lors de l'exécution de l'opération de taille. Si les résultats obtenus par deux opérations consécutives non verrouillées sont cohérents, le résultat peut être considéré comme correct.

Le nombre de tentatives est défini à l'aide de RETRIES_BEFORE_LOCK, qui a la valeur 2. La valeur initiale des tentatives est -1, donc le nombre de tentatives est 3.

Si le nombre de tentatives dépasse 3, chaque segment doit être verrouillé.

static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; 
    long sum;        
    long last = 0L; 
    int retries = -1;
    try {
        for (;;) {
            // 超过尝试次数,则对每个 Segment 加锁
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); 
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            // 连续两次得到的结果一致,则认为这个结果是正确的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}
3. Modifications du JDK 1.8

JDK 1.7 utilise le mécanisme de verrouillage de segment pour implémenter des opérations de mise à jour simultanées. La classe principale est Segment, qui hérite du verrouillage réentrant ReentrantLock. Le degré de concurrence est égal au nombre de segments.

JDK 1.8 utilise les opérations CAS pour prendre en charge une concurrence plus élevée et utilise un verrou intégré synchronisé en cas d'échec des opérations CAS.

Et l'implémentation du JDK 1.8 sera également convertie en une arborescence rouge-noir lorsque la liste chaînée est trop longue.

LinkedHashMap

structure de stockage

Hérité de HashMap, il possède les mêmes caractéristiques de recherche rapide que HashMap.

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

Une liste doublement chaînée est maintenue en interne pour maintenir l'ordre d'insertion ou l'ordre LRU.

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;

accessOrder détermine l'ordre, et la valeur par défaut est false. À ce stade, l'ordre d'insertion est conservé.

final boolean accessOrder;

La chose la plus importante à propos de LinkedHashMap réside dans les fonctions suivantes pour maintenir l'ordre, qui seront appelées dans les méthodes put, get et autres.

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
aprèsNodeAccess()

Lors de l'accès à un nœud, si accessOrder est vrai, le nœud sera déplacé à la fin de la liste chaînée. C'est-à-dire qu'après avoir spécifié l'ordre LRU, chaque fois qu'un nœud est accédé, le nœud sera déplacé vers la fin de la liste chaînée pour garantir que la fin de la liste chaînée est le nœud le plus récemment visité, et le chef de la liste chaînée est le nœud inutilisé le plus récent et le plus long.

void afterNodeAccess(Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
aprèsNodeInsertion()

Il est exécuté après des opérations telles que put. Lorsque la méthode removeEldestEntry() renvoie true, le dernier nœud, qui est le premier nœud de la liste chaînée, sera supprimé.

evict est faux uniquement lors de la construction de la carte, ici c'est vrai.

void afterNodeInsertion(boolean evict) { 
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

Je suppose que tu aimes

Origine blog.csdn.net/XikYu/article/details/132041595
conseillé
Classement