Análisis de código fuente HashMap (jdk7)

Enlace original: https://www.cnblogs.com/fsmly/p/11278561.html

Diagrama de estructura HashMap

El HashMap de jdk1.7 se implementa mediante matriz + lista enlazada individualmente. Aunque la función hash está definida para evitar conflictos, debido a que la longitud de la matriz es limitada, dos claves diferentes tendrán la misma posición en la matriz después del cálculo. se adopta en la versión 1.7 Para solucionar el problema con una lista enlazada.

En el diagrama simple anterior, también se puede encontrar que si hay demasiados nodos en la lista vinculada, entonces es obvio que la eficiencia de la búsqueda secuencial a través del valor clave es demasiado baja, por lo que se ha mejorado en 1.8, usando matriz + lista vinculada + rojo Se implementa el árbol negro. Cuando la longitud de la lista vinculada excede el umbral 8, la lista vinculada se convierte en un árbol rojo-negro. Para obtener más información, consulte la comprensión en profundidad de HashMap en jdk8 resumido en mi artículo anterior  .

De la figura anterior, también sabemos que cada elemento es en realidad un tipo de Entrada, así que echemos un vistazo a qué atributos están en Entrada (en 1.8, Entrada se renombró Nodo y también se implementó Map.Entry).

//hash标中的结点Node,实现了Map.Entry
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
	//Entry构造器,需要key的hash,key,value和next指向的结点
    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;
    }
    //equals方法
    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;
    }
	//重写Object的hashCode
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }

	//调用put(k,v)方法时候,如果key相同即Entry数组中的值会被覆盖,就会调用此方法。
    void recordAccess(HashMap<K,V> m) {
    }

    //只要从表中删除entry,就会调用此方法
    void recordRemoval(HashMap<K,V> m) {
    }
}

volver a la cima

Variables miembro en HashMap y su significado

//默认初始化容量初始化=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量 = 1 << 30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认加载因子.一般HashMap的扩容的临界点是当前HashMap的大小 > DEFAULT_LOAD_FACTOR * 
//DEFAULT_INITIAL_CAPACITY = 0.75F * 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//默认是空的table数组
static final Entry<?,?>[] EMPTY_TABLE = {};

//table[]默认也是上面给的EMPTY_TABLE空数组,所以在使用put的时候必须resize长度为2的幂次方值
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//map中的实际元素个数 != table.length
transient int size;

//扩容阈值,当size大于等于其值,会执行resize操作
//一般情况下threshold=capacity*loadFactor
int threshold;

//hashTable的加载因子
final float loadFactor;

/**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
transient int modCount;

//hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算
//hashSeed是一个与实例相关的随机值,用于解决hash冲突
//如果为0则禁用备用哈希算法
transient int hashSeed = 0;

volver a la cima

El método de construcción de HashMap

Echemos un vistazo a los cuatro métodos de construcción que se nos proporcionan en el código fuente de HashMap.

//(1)无参构造器:
//构造一个空的table,其中初始化容量为DEFAULT_INITIAL_CAPACITY=16。加载因子为DEFAULT_LOAD_FACTOR=0.75F
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//(2)指定初始化容量的构造器
//构造一个空的table,其中初始化容量为传入的参数initialCapacity。加载因子为DEFAULT_LOAD_FACTOR=0.75F
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//(3)指定初始化容量和加载因子的构造器
//构造一个空的table,初始化容量为传入参数initialCapacity,加载因子为loadFactor
public HashMap(int initialCapacity, float loadFactor) {
    //对传入初始化参数进行合法性检验,<0就抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果initialCapacity大于最大容量,那么容量=MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //对传入加载因子参数进行合法检验,
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        //<0或者不是Float类型的数值,抛出异常
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
	//两个参数检验完了,就给本map实例的属性赋值
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    //init是一个空的方法,模板方法,如果有子类需要扩展可以自行实现
    init();
}

A partir de los tres métodos de construcción anteriores, podemos encontrar que aunque se especifica la capacidad inicial, la tabla en este momento todavía está vacía, una matriz vacía, y el umbral del umbral de expansión es la capacidad dada o la capacidad predeterminada (las dos primeras construcciones En realidad, el método se realiza llamando al tercero). Antes de su operación put, se creará una matriz (similar al uso de la construcción sin parámetros en jdk8).

//(4)参数为一个map映射集合
//构造一个新的map映射,使用默认加载因子,容量为参数map大小除以默认负载因子+1与默认容量的最大值
public HashMap(Map<? extends K, ? extends V> m) {
    //容量:map.size()/0.75+1 和 16两者中更大的一个
    this(Math.max(
        	(int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), 
         DEFAULT_LOAD_FACTOR);
    inflateTable(threshold);
    //把传入的map里的所有元素放入当前已构造的HashMap中
    putAllForCreate(m);
}

Este método de construcción es llamar al método inflateTable antes de la operación put. La función específica de este método es crear una nueva tabla para usarla posteriormente putAllForCreate para cargar los elementos en el mapa entrante. Echemos un vistazo a este método y prestemos atención Se menciona que el umbral de expansión umbral en este momento es la capacidad inicial. Algunos de estos métodos se explican a continuación.

(1) Descripción del método InflateTable

Este método es más importante, se llama en el cuarto constructor. Y si se utilizan los primeros tres constructores al crear el objeto de colección, este método se llamará cuando se llame al método put para inicializar la tabla.

private void inflateTable(int toSize) {
    //返回不小于number的最小的2的幂数,最大为MAXIMUM_CAPACITY,类比jdk8的实现中的tabSizeFor的作用
    int capacity = roundUpToPowerOf2(toSize);
	//扩容阈值为:(容量*加载因子)和(最大容量+1)中较小的一个
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //创建table数组
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

(2) Descripción del método RoundUpToPowerOf

private static int roundUpToPowerOf2(int number) {
    //number >= 0,不能为负数,
    //(1)number >= 最大容量:就返回最大容量
    //(2)0 =< number <= 1:返回1
    //(3)1 < number < 最大容量:
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//该方法和jdk8中的tabSizeFor实现基本差不多
public static int highestOneBit(int i) {
    //因为传入的i>0,所以i的高位还是0,这样使用>>运算符就相当于>>>了,高位0。
    //还是举个例子,假设i=5=0101
    i |= (i >>  1); //(1)i>>1=0010;(2)i= 0101 | 0010 = 0111
    i |= (i >>  2); //(1)i>>2=0011;(2)i= 0111 | 0011 = 0111
    i |= (i >>  4); //(1)i>>4=0000;(2)i= 0111 | 0000 = 0111
    i |= (i >>  8); //(1)i>>8=0000;(2)i= 0111 | 0000 = 0111
    i |= (i >> 16); //(1)i>>16=0000;(2)i= 0111 | 0000 = 0111
    return i - (i >>> 1); //(1)0111>>>1=0011(2)0111-0011=0100=4
    //所以这里返回4。
    //而在上面的roundUpToPowerOf2方法中,最后会将highestOneBit的返回值进行 << 1 操作,即最后的结果为4<<1=8.就是返回大于number的最小2次幂
}

(3) Descripción del método PutAllForCreate

El método consiste en atravesar los elementos de la colección de mapas entrantes y luego agregarlos a esta instancia de mapa. Echemos un vistazo a los detalles de implementación de este método.

private void putAllForCreate(Map<? extends K, ? extends V> m) {
    //实际上就是遍历传入的map,将其中的元素添加到本map实例中(putForCreate方法实现)
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putForCreate(e.getKey(), e.getValue());
}

El principio del método putForCreate

private void putForCreate(K key, V value) {
    //判断key是否为null,如果为null那么对应的hash为0,否则调用刚刚上面说到的hash()方法计算hash值
    int hash = null == key ? 0 : hash(key); 
    //根据刚刚计算得到的hash值计算在table数组中的下标
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash相同,key也相同,直接用旧的值替换新的值
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
	//这里就是:要插入的元素的key与前面的链表中的key都不相同,所以需要新加一个结点加入链表中
    createEntry(hash, key, value, i);
}

(4) Implementación del método CreateEntry

void createEntry(int hash, K key, V value, int bucketIndex) {
    //这里说的是,前面的链表中不存在相同的key,所以调用这个方法创建一个新的结点,并且结点所在的桶
    //bucket的下标指定好了
    Entry<K,V> e = table[bucketIndex];
    /*Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}*/
    table[bucketIndex] = new Entry<>(hash, key, value, e);//Entry的构造器,创建一个新的结点作为头节点(头插法)
    size++;//将当前hash表中的数量加1
}

volver a la cima

HashMap determina la posición del elemento en la matriz

El algoritmo para calcular el valor hash en 1.7 es diferente de la implementación de 1.8, y el valor hash está relacionado con la posición de nuestro elemento put new, get encuentra el elemento, remove elimina el elemento para encontrar el subíndice a través de indexFor. Así que echemos un vistazo a estos dos métodos.

(1) Método hash

final int hash(Object k) {
    int h = hashSeed;
    //默认是0,不是0那么需要key是String类型才使用stringHash32这种hash方法
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    //这段代码是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点
    //说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对
    //最终得到的结果产生影响
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

Usamos el siguiente ejemplo para ilustrar la importancia de perturbar el código hash de la clave. Ahora queremos poner un par clave-valor en un mapa. El valor de la clave es "fsmly" y no se requiere ninguna perturbación. El conocimiento es puro Después de simplemente obtener el código hash , el valor obtenido es " 0000_0000_0011_0110_0100_0100_1001_0010 ". Si la longitud de la matriz de la tabla en el mapa actual es 16, el valor del resultado del índice final es 10. Dado que el binario de 15 se expande a 32 bits como "00000000000000000000000000001111", cuando un número está en operación AND bit a bit con él, sin importar cuáles sean los primeros 28 bits, el resultado del cálculo es el mismo (porque 0 está asociado con cualquier número, el resultado es tanto si es 0, entonces el nodo de entrada de una opción de venta depende demasiado del valor bajo del código hash de la clave, y la probabilidad de conflicto también aumentará en gran medida). Como se muestra abajo

Debido a que la longitud de la matriz de mapas es limitada, el método con alta probabilidad de conflicto no es adecuado para su uso, por lo que hashCode debe modificarse para reducir la probabilidad de conflicto, y JDK7 utiliza operaciones de cuatro bits para este procesamiento, o mediante el siguiente Eche un vistazo a este proceso como un ejemplo simple. Puede ver que el hashCode que no ha sido alterado en este momento no tiene conflictos de hash después del procesamiento.

Para resumir: primero calcularemos el valor hash de la clave pasada y luego usaremos el método indexFor a continuación para determinar la posición en la tabla. La implementación específica es realizar una operación de bit a través de un valor hash calculado y una longitud-1, luego, para 2 ^ Para n, después de que la longitud menos uno se convierta en binario, el de menor orden es todo uno (longitud 16, len-1 = 15, binario es 1111). La ventaja de esta configuración de las cuatro perturbaciones anteriores es que cada bit del hashCode obtenido afectará la determinación de nuestra posición en el índice. El propósito es hacer que los datos tengan un mejor hash en diferentes cubos y reducir el hash. La ocurrencia de conflictos. Para obtener más principios y detalles del método hash en las colecciones de Java, consulte este análisis del método hash ()

(2) método indexFor

static int indexFor(int h, int length) {
    //还是使用hash & (n - 1)计算得到下标
    return h & (length-1);
}

La implementación principal es realizar una operación AND bit a bit entre el valor hash de la clave calculada y la longitud-1 de la matriz en el mapa para obtener el subíndice de matriz de la entrada colocada en la tabla. El proceso de cálculo específico también se ilustra en la introducción del método hash anterior, por lo que no entraré en detalles aquí.

volver a la cima

Análisis del método put de HashMap

(1) método put

public V put(K key, V value) {
    //我们知道Hash Map有四中构造器,而只有一种(参数为map的)初始化了table数组,其余三个构造器只
    //是赋值了阈值和加载因子,所以使用这三种构造器创建的map对象,在调用put方法的时候table为{},
    //其中没有元素,所以需要对table进行初始化
    if (table == EMPTY_TABLE) {
        //调用inflateTable方法,对table进行初始化,table的长度为:
        //不小于threshold的最小的2的幂数,最大为MAXIMUM_CAPACITY
        inflateTable(threshold);
    }
    //如果key为null,表示插入一个键为null的K-V对,需要调用putForNullKey方法
    if (key == null)
        return putForNullKey(value);
    
    //计算put传入的key的hash值
    int hash = hash(key);
    //根据hash值和table的长度计算所在的下标
    int i = indexFor(hash, table.length);
    //从数组中下标为indexFor(hash, table.length)处开始(1.7中是用链表解决hash冲突的,这里就
    //是遍历链表),实际上就是已经定位到了下标i,这时候就需要处理可能出现hash冲突的问题
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash值相同,key相同,替换该位置的oldValue为value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            //空方法,让其子类重写
            e.recordAccess(this);
            return oldValue;
        }
    }
	//如果key不相同,即在链表中没有找到相同的key,那么需要将这个结点加入table[i]这个链表中
    
    //修改modCount值(后续总结文章会说到这个问题)
    modCount++;
    //遍历没有找到该key,就调用该方法添加新的结点
    addEntry(hash, key, value, i);
    return null;
}

(2) Análisis del método putForNullKey

Este método se ocupa del caso en el que la clave es nula. Cuando la clave pasada es nula, comenzará a atravesar en la posición de la tabla [0]. El recorrido es en realidad la lista vinculada actual con la tabla [0] como encabezado nodo. Si se encuentra que la clave del nodo en la lista vinculada es nula, entonces el valor anterior se reemplaza directamente con el valor pasado. De lo contrario, cree un nuevo nodo y agréguelo a la posición de la tabla [0].

//找到table数组中key为null的那个Entry对象,然后将其value进行替换
private V putForNullKey(V value) {
    //从table[0]开始遍历
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //key为null
        if (e.key == null) {
            //将value替换为传递进来的value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue; //返回旧值
        }
    }
    modCount++;
    //若不存在,0位置桶上的链表中添加新结点
    addEntry(0, null, value, 0);
    return null;
}

(3) Análisis del método addEntry

La función principal del método addEntry es determinar si el tamaño actual es mayor que el umbral, y luego determinar si expandir según el resultado, y finalmente crear un nuevo nodo e insertarlo en la cabecera de la lista vinculada (en realidad la posición del subíndice especificada en la matriz de la tabla)

/*
	hashmap采用头插法插入结点,为什么要头插而不是尾插,因为后插入的数据被使用的频次更高,而单链表无法随机访问只能从头开始遍历查询,所以采用头插.突然又想为什么不采用二维数组的形式利用线性探查法来处理冲突,数组末尾插入也是O(1),可数组其最大缺陷就是在于若不是末尾插入删除效率很低,其次若添加的数据分布均匀那么每个桶上的数组都需要预留内存.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
    //这里有两个条件
    //①size是否大于阈值
    //②当前传入的下标在table中的位置不为null
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //如果超过阈值需要进行扩容
        resize(2 * table.length);
        //下面是扩容之后的操作
        //计算不为null的key的hash值,为null就是0
        hash = (null != key) ? hash(key) : 0;
        //根据hash计算下标
        bucketIndex = indexFor(hash, table.length);
    }
    //执行到这里表示(可能已经扩容也可能没有扩容),创建一个新的Entry结点
    createEntry(hash, key, value, bucketIndex);
}

(4) Resumir el proceso de ejecución del método put

  1. Primero determine si la matriz está vacía, si es un acondicionador de aire, use inflateTable para expandir.
  2. Luego, determine si la clave es nula, si es nula, llame al método putForNullKey para poner. (Aquí también se muestra que HashMap permite que la clave sea nula y el valor predeterminado se inserta en la posición 0 en la tabla).
  3. Llame al método hash (), realice un cálculo hash en la clave y realice y calcule el valor hash y la longitud actual de la matriz para obtener el índice en la matriz
  4. Luego, recorra la lista vinculada debajo del índice de la matriz. Si el hash de la clave es el mismo que el hash de la clave pasada y los iguales de la clave se vuelven verdaderos, entonces el valor se sobrescribe directamente
  5. Finalmente, si no existe, inserte y cree un nuevo nodo en esta lista vinculada

volver a la cima

Análisis del método de cambio de tamaño de HashMap

(1) El proceso general de cambio de tamaño

void resize(int newCapacity) {
    //获取map中的旧table数组暂存起来
    Entry[] oldTable = table;
    //获取原table数组的长度暂存起来
    int oldCapacity = oldTable.length;
    //如果原table的容量已经超过了最大值,旧直接将阈值设置为最大值
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
	//以传入的新的容量长度为新的哈希表的长度,创建新的数组
    Entry[] newTable = new Entry[newCapacity];
    //调用transfer
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //table指向新的数组
    table = newTable;
    //更新阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

(2) Análisis del método de transferencia

El método de transferencia atraviesa todas las entradas en la matriz anterior, recalcula los encabezados de índice uno por uno según la nueva capacidad, los inserta y almacena en la nueva matriz.

void transfer(Entry[] newTable, boolean rehash) {
    //新数组的长度
    int newCapacity = newTable.length;
    //遍历旧数组
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                //重新计算hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //这里根据刚刚得到的新hash重新调用indexFor方法计算下标索引
            int i = indexFor(e.hash, newCapacity);
            //假设当前数组中某个位置的链表结构为a->b->c;women 
            //(1)当为原链表中的第一个结点的时候:e.next=null;newTable[i]=e;e=e.next
            //(2)当遍历到原链表中的后续节点的时候:e.next=head;newTable[i]=e(这里将头节点设置为新插入的结点,即头插法);e=e.next
            //(3)这里也是导致扩容后,链表顺序反转的原理(代码就是这样写的,链表反转,当然前提是计算的新下标还是相同的)
            e.next = newTable[i]; 
            newTable[i] = e;
            e = next;
        }
    }
}

La parte principal de este método es que después de recalcular el hash, la diferencia entre la lista vinculada original y la estructura de la lista vinculada de la nueva tabla se puede entender a través del siguiente diagrama simple, asumiendo que la posición 4 en la tabla original es un vínculo vinculado list entry1-> entry2-> entry3, el cálculo del subíndice de los tres nodos en la nueva matriz sigue siendo 4, entonces el proceso es aproximadamente como se muestra en la siguiente figura

(3) Resumen del método de expansión de cambio de tamaño

  1. Cree una nueva matriz (la longitud es el doble de la longitud original, si ha superado el valor máximo, establézcalo en el valor máximo)
  2. Llame al método de transferencia para mover la entrada de la tabla anterior a la nueva matriz, los detalles se muestran arriba
  3. Apunte la tabla a la nueva tabla y actualice el umbral

volver a la cima

Análisis del método get de HashMap

//get方法,其中调用的是getEntry方法没如果不为null就返回对应entry的value
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}

Como puede ver, el método get llama a getEntry para consultar el objeto Entry y luego devuelve el valor de Entry. Así que echemos un vistazo a la implementación del método getEntry

método getEntry

//这是getEntry的实现
final Entry<K,V> getEntry(Object key) {
    //没有元素自然返回null
    if (size == 0) {
        return null;
    }
	//通过传入的key值调用hash方法计算哈希值
    int hash = (key == null) ? 0 : hash(key);
    //计算好索引之后,从对应的链表中遍历查找Entry
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //hash相同,key相同就返回
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

método getForNullKey

//这个方法是直接查找key为null的
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    //直接从table中下标为0的位置处的链表(只有一个key为null的)开始查找
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //key为null,直接返回对应的value
        if (e.key == null)
            return e.value;
    }
    return null;
}

volver a la cima

Un simple resumen de la implementación de la versión jdk7

(1) Debido a que la operación put usa el método putForNullKey para lidiar con el escenario donde la clave es nula por separado, HashMap permite nulo como clave

(2) Al calcular el subíndice de la tabla, llame al método hash () de acuerdo con el valor del código hash de la clave para obtener el valor hash y realice la operación & con la matriz length-1. Los bits binarios de length-1 son todos 1, que es para poder distribuir uniformemente para evitar conflictos (se requiere que la longitud sea una potencia entera de 2)
(3) Ya sea obtener, poner o redimensionar, el código hash de la clave será hash durante el proceso de ejecución, y el código hash del objeto variable es fácil de cambiar. Por lo tanto, HashMap recomienda usar objetos inmutables (como el tipo de cadena) como clave.
(4) HashMap no es seguro para subprocesos y la expansión en un entorno de subprocesos múltiples puede causar un bucle sin fin de la lista enlazada circular, por lo que si necesita operar en un escenario de subprocesos múltiples Use ConcurrentHashMap (demostraremos brevemente esta situación a través del siguiente diagrama)
(5) Cuando ocurre un conflicto, HashMap usa la cadena método de dirección para manejar el conflicto.
(6) La capacidad inicial del HashMap se establece en 16, y si simplemente se considera que es 8, el umbral de expansión es 6. El umbral es demasiado pequeño, lo que resulta en una expansión frecuente; y 32 , la tasa de utilización del espacio puede ser baja.

volver a la cima

El problema de la lista enlazada circular en la concurrencia jdk7

Cuando hablamos del método de cambio de tamaño anterior, también explicamos un proceso de cambio de tamaño a través de un ejemplo ilustrado, por lo que aquí no demostraremos el flujo de ejecución en un solo hilo. Primero recordamos algunas líneas de código central en el método de cambio de tamaño

Entry<K,V> next = e.next;
//省略重新计算hash和index的两个过程...
e.next = newTable[i]; 
newTable[i] = e;
e = next;

Las principales líneas de código del método de transferencia llamado en el método de cambio de tamaño son las cuatro líneas anteriores Simulemos brevemente el proceso de asumir que los dos subprocesos thread1 y thread2 realizan el cambio de tamaño.

(1) Antes de cambiar el tamaño, suponga que la longitud de la tabla es 2, suponga que ahora que agrega una entrada 4, necesita expandir

(2) Suponiendo que thread1 ahora se ejecuta en  Entry <K, V> next = e.next; esta línea de código, luego basada en las líneas de código anteriores, simplemente hacemos un comentario

(3) Entonces, debido a que la programación de subprocesos es el turno de ejecución de thread2, suponga que thread2 finaliza el método de transferencia (asumiendo que entry3 y entry4 alcanzan la posición que se muestra en la siguiente figura después de la expansión, aquí nos enfocamos principalmente en dos nodos de entry1 y entry2), obtenemos El resultado es

(4) En este momento, el hilo1 está programado para continuar la ejecución, inserta la entrada1 en la nueva matriz y luego e es la entrada 2. Cuando es el siguiente ciclo, el siguiente se convierte en la entrada1 debido a la operación de Thread2.

  • Primero ejecute newTalbe [i] = e; cuando se ejecuta thread1, e apunta a entry1
  • Entonces e = siguiente, lo que hace que e apunte a la entrada2 (el siguiente apunta a la entrada2)
  • Y el siguiente ciclo de next = e.next, (es decir, next = entry2.next = entry1, que es el resultado de la ejecución de thread2) causado al lado del punto a entry1

Como se muestra abajo

(5) Thread1 continúa la ejecución, toma entry2 hacia abajo, la coloca en la primera posición del depósito de newTable [1], y luego mueve ey next

(6) e.next = newTable [1] hace que entry1.next apunte a entry2 . También tenga en cuenta que entry2.next en este momento ya apunta a entry1 (el resultado de la ejecución de thread2 es entry2-> entry1, vea se ejecuta thread2 arriba Diagrama esquemático de), acaba de aparecer la lista circular enlazada.

Supongo que te gusta

Origin blog.csdn.net/u013804636/article/details/107834974
Recomendado
Clasificación