HashMap源码分析与常见八股

HashMap源码分析与常见八股

源码分析

基本属性(阈值&系数&容量)

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
	// 序列号
    @java.io.Serial
    private static final long serialVersionUID = 362498820763181265L;

	// 默认容量,也就是 16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

	/**
	 * 最大容量,如果通过构造函数隐式指定了更高的值,则使用此值。
	 * 必须是2的幂且 <= 1<<30。
	 * 也就是最大容量是 2^30
	 */
	static final int MAXIMUM_CAPACITY = 1 << 30;

	/**
	 * 构造函数未指定时使用的负载因子。
	 */
	static final float DEFAULT_LOAD_FACTOR = 0.75f;

	// 当桶上的节点数>=这个值时会转化为红黑树
	static final int TREEIFY_THRESHOLD = 8;

	// 当桶上的节点数小于这个值时会转化为链表
	static final int UNTREEIFY_THRESHOLD = 6;

	// 转化为红黑树对应的所需要的最小的数组容量
	static final int MIN_TREEIFY_CAPACITY = 64;
	// 存储元素的数组,容量总是2的幂次倍
	transient Node<K,V>[] table;
	// transient用于标识这个字段不应该被序列化
	transient Set<Map.Entry<K,V>> entrySet;
	// 存放元素的个数,
	transient int size;
	// 每次扩容和更改map结构的计数器
	/*
	* 1.检测并发修改:在迭代过程中,如果 modCount 发生变化,迭代器会抛出 ConcurrentModificationException,以防止并发修改导致的不一致性。
	2.维护内部状态:在某些操作(如插入、删除、扩容)中,modCount 会增加,以确保 HashMap 的内部状态保持一致
	*/
	transient int modCount;
	// 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
	int threshold;
	// 负载因子
	final float loadFactor;
}
loadFactor负载因子

负载因子是控制数组存放数据的疏密程度,loadFactor越接近1,那么数组中存放的数据(entry)也就越多,否则就越少。
太大查找元素的效率低,太小利用率太低
所以默认 16 * 0.75 = 12,超过这个数据量时,就会进行扩容

threshold

threshold = capacity * loadFacotry,是数组扩容的标准因为hashmap中没有capacity这个属性,所以即使指定了初始化的capacity,也会被扩容到2的最接近这个大小的幂次方

Node 节点类源码

// 继承Map.Entry<K,V>
static class Node<K,V> implements Map.Entry<K,V> {  
	// hash值,存放元素到hashmap时用来与其他元素进行比较
    final int hash;  
    final K key;  
    V value;  
	// 指向下一个节点
    Node<K,V> next;  
  
    Node(int hash, K key, V value, Node<K,V> next) {  
        this.hash = hash;  
        this.key = key;  
        this.value = value;  
        this.next = next;  
    }  
    public final K getKey()        { return key; }  
    public final V getValue()      { return value; }  
    public final String toString() { return key + "=" + value; }  
  
    public final int hashCode() {  
        return Objects.hashCode(key) ^ Objects.hashCode(value);  
    }  
    public final V setValue(V newValue) {  
        V oldValue = value;  
        value = newValue;  
        return oldValue;  
    }  
    public final boolean equals(Object o) {  
        if (o == this)  
            return true;  
  
        return o instanceof Map.Entry<?, ?> e  
                && Objects.equals(key, e.getKey())  
                && Objects.equals(value, e.getValue());  
    }}

树节点源码

// 仅截取部分源码
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {  
    TreeNode<K,V> parent;  // 父
    TreeNode<K,V> left;    // 左
    TreeNode<K,V> right;   // 右 
    TreeNode<K,V> prev;    // needed to unlink next upon deletion  
    boolean red;
}

hash方法

put

put方法实际就是调用putVal,并且putVal用户不可直接使用

public V put(K key, V value) {  
    return putVal(hash(key), key, value, false, true);  
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果表为空或长度为0,则进行初始化或扩容
    // 将属性中的table赋值,并且判断是否为空或者长度为0,如果是就进行初始化或者扩容
    if ((tab = table) == null || (n = tab.length) == 0)
	    // resize()函数就是进行扩容或者初始化
        n = (tab = resize()).length;
    // 计算索引位置,如果当前位置为空,则直接插入新节点
    // (n - 1) & hash 确定元素放在哪个桶里,桶为空,新生成节点放入数组中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果当前位置的节点的哈希值和键都相等,则直接覆盖
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果是树节点,则调用树节点的插入方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 遍历链表,查找插入位置或相同键的节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果链表长度达到阈值,则转换为树结构
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果找到相同键的节点,则跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果找到相同键的节点,则更新值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 修改计数器
    ++modCount;
    // 如果大小超过阈值,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

基本逻辑:

  1. 如果定位到的数组位置没有元素,则直接插入
  2. 如果有元素,就要与插入的key进行比较,如果相同,就覆盖。如果不同就判断p是否是树节点,如果不是就直接遍历链表插入,使用尾插法,否则调用putTreeVal函数

get

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
    // 检查表是否为空且长度大于0,并计算哈希值和索引
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & (hash = hash(key))]) != null) {
        // 检查第一个节点是否匹配
        if (first.hash == hash && // 始终检查第一个节点
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 遍历链表或树节点
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 检查链表中的每个节点是否匹配
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 如果没有找到匹配的节点,返回null
    return null;
}

resize

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 旧的哈希表
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧的容量
    int oldThr = threshold; // 旧的阈值
    int newCap, newThr = 0; // 新的容量和阈值
    if (oldCap > 0) { // 如果旧的容量大于0
        if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧的容量已经达到最大容量
            threshold = Integer.MAX_VALUE; // 将阈值设为最大整数值
            return oldTab; // 返回旧的哈希表
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 将容量和阈值都翻倍
    }
    else if (oldThr > 0) // 如果旧的阈值大于0
        newCap = oldThr; // 将新的容量设为旧的阈值
    else { // 如果旧的阈值为0,使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY; // 默认初始容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认阈值
    }
    if (newThr == 0) { // 如果新的阈值为0
        float ft = (float)newCap * loadFactor; // 计算新的阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 更新阈值
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的哈希表
    table = newTab; // 更新哈希表引用
    if (oldTab != null) { // 如果旧的哈希表不为空
        for (int j = 0; j < oldCap; ++j) { // 遍历旧的哈希表
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { // 如果旧表中的桶不为空
                oldTab[j] = null; // 清空旧表中的桶
                if (e.next == null) // 如果桶中只有一个节点
                    newTab[e.hash & (newCap - 1)] = e; // 直接放入新表
                else if (e instanceof TreeNode) // 如果桶中是树节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 拆分树节点
                else { // 如果桶中是链表节点
                    Node<K,V> loHead = null, loTail = null; // 低位链表的头和尾
                    Node<K,V> hiHead = null, hiTail = null; // 高位链表的头和尾
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) { // 低位链表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else { // 高位链表
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead; // 放入低位链表
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead; // 放入高位链表
                    }
                }
            }
        }
    }
    return newTab; // 返回新的哈希表
}

ConcurrentHashMap

JDK1.7

JDK1.7中,ConcurrentHashMap是由多个Segment组合,而每一个Segment都是一个类似于HashMap的结构,可以内部进行扩容,但是Segment的个数一旦初始化就不能改变了。默认支持16个,也就是默认支持16个线程并发

初始化

无参构造中调用了有参构造,传入三个默认值

/**
 * 默认初始化容量
 */
static final int DEFAULT_INITIAL_CAPACITY = 16;

/**
 * 默认负载因子
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 默认并发级别
 */
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

初始化逻辑

@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
    // 参数校验
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 校验并发级别大小,大于 1<<16,重置为 65536
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    // 2的多少次方
    int sshift = 0;
    int ssize = 1;
    // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // 记录段偏移量
    this.segmentShift = 32 - sshift;
    // 记录段掩码
    this.segmentMask = ssize - 1;
    // 设置容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    // 创建 Segment 数组,设置 segments[0]
    Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}
  1. 校验并发等级大小,如果大于最大值,就重置为最大值
  2. 初始化容量为大于并发等级最近的2的幂次方,默认16
  3. 记录segmentShift偏移量,2^n 中的n,默认为32 - sshift = 28
  4. 记录segmentMask,默认是ssize - 1 = 16 - 1 = 15
  5. 初始化segment[0],默认大小为2,扩容阈值为 2 * 0.75 = 1.5。插入第二个值时才会进行扩容

put

/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p> The value can be retrieved by calling the <tt>get</tt> method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>
 * @throws NullPointerException if the specified key or value is null
 */
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算
    // 其实也就是把高4位与segmentMask(1111)做与运算
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        // 如果查找到的 Segment 为空,初始化
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

/**
 * Returns the segment for the given index, creating it and
 * recording in segment table (via CAS) if not already present.
 *
 * @param k the index
 * @return the segment
 */
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    // 判断 u 位置的 Segment 是否为null
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        // 获取0号 segment 里的 HashEntry<K,V> 初始化长度
        int cap = proto.table.length;
        // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的
        float lf = proto.loadFactor;
        // 计算扩容阀值
        int threshold = (int)(cap * lf);
        // 创建一个 cap 容量的 HashEntry 数组
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck
            // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 自旋检查 u 位置的 Segment 是否为null
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                // 使用CAS 赋值,只会成功一次
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}
  1. 计算key的位置,获取指定位置的Segment
  2. 如果指定位置的Segment为空,则初始化这个Segment
    • 如果为null,使用Segment[0]的容量和负载因子创建一个HashEntry数组
    • 再次检查是否为null
    • 使用HashEntry初始化这个Segment
    • 自旋计算得到的指定位置是否为null,使用CAS在这个位置赋值Segment
  3. Segment.put插入key,value
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。
        HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
        V oldValue;
        try {
            HashEntry<K,V>[] tab = table;
            // 计算要put的数据位置
            int index = (tab.length - 1) & hash;
            // CAS 获取 index 坐标的值
            HashEntry<K,V> first = entryAt(tab, index);
            for (HashEntry<K,V> e = first;;) {
                if (e != null) {
                    // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value
                    K k;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        oldValue = e.value;
                        if (!onlyIfAbsent) {
                            e.value = value;
                            ++modCount;
                        }
                        break;
                    }
                    e = e.next;
                }
                else {
                    // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。
                    if (node != null)
                        node.setNext(first);
                    else
                        node = new HashEntry<K,V>(hash, key, value, first);
                    int c = count + 1;
                    // 容量大于扩容阀值,小于最大容量,进行扩容
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        rehash(node);
                    else
                        // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break;
                }
            }
        } finally {
            unlock();
        }
        return oldValue;
    }
  • tryLock() 获取锁,获取不到使用 scanAndLockForPut 方法继续获取。

  • 计算 put 的数据要放入的 index 位置,然后获取这个位置上的 HashEntry 。

  • 遍历 put 新元素,为什么要遍历?因为这里获取的 HashEntry 可能是一个空元素,也可能是链表已存在,所以要区别对待。

    如果这个位置上的 HashEntry 不存在

    1. 如果当前容量大于扩容阀值,小于最大容量,进行扩容
    2. 直接头插法插入。

    如果这个位置上的 HashEntry 存在

    1. 判断链表当前元素 key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值
    2. 不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。
      1. 如果当前容量大于扩容阀值,小于最大容量,进行扩容
      2. 直接链表头插法插入。
  • 如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null.

resize

扩容使用的是头插法

JDK1.8

由原先的Segment数组 + HashEntry转化为Node数组 + 链表/红黑树

重要属性

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>  
    implements ConcurrentMap<K,V>, Serializable {
    transient volatile Node<K,V>[] table; // 哈希表数组,存储键值对

/**
 * 下一个哈希表数组,仅在扩容时使用,不为空
 */
private transient volatile Node<K,V>[] nextTable;

/**
 * 基础计数器值,主要在没有竞争时使用,也作为表初始化竞争期间的回退。通过CAS更新
 */
private transient volatile long baseCount;

/**
 * 表初始化和扩容控制。当为负数时,表示表正在初始化或扩容:-1表示初始化,否则为-(1 + 活动扩容线程数)。
 * 否则,当表为空时,保存创建时使用的初始表大小,默认为0。初始化后,保存下一个用于扩容的元素计数值
 */
private transient volatile int sizeCtl;

/**
 * 扩容时要拆分的下一个表索引(加一)
 */
private transient volatile int transferIndex;

/**
 * 扩容和/或创建CounterCells时使用的自旋锁(通过CAS锁定)
 */
private transient volatile int cellsBusy;

/**
 * 计数单元表。当不为空时,大小为2的幂
 */
private transient volatile CounterCell[] counterCells;

// views
private transient KeySetView<K,V> keySet; // 键集合视图
private transient ValuesView<K,V> values; // 值集合视图
private transient EntrySetView<K,V> entrySet; // 键值对集合视图
}

Node

基本与HashMap中的相同,所以略过

initTable

初始化

private final Node<K,V>[] initTable() {
       Node<K,V>[] tab; int sc;
       while ((tab = table) == null || tab.length == 0) {
        // sizeCtl < 0 说明其他线程CAS成功,正在初始化或
           if ((sc = sizeCtl) < 0)
			// 让出cpu
               Thread.yield(); 
           else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
               try {
                   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;
   }
  1. 通过自旋 + CAS操作来完成初始化
  2. sizeCtl 的值
    • -1 : 正在初始化
      • N:正在扩容高16位位扩容的标识戳,低16位-1位正在扩容的线程数
    • 0 :table初始化大小,如果table没有初始化
    • 0 : 扩容阈值

put

public V put(K key, V value) {  
    return putVal(key, value, false);  
}  
  
/** Implementation for put and putIfAbsent */  
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;;) {  
	    // f是目标元素的位置, fh是后面存放目标位置元素的hash值
        Node<K,V> f; int n, i, fh;  
        if (tab == null || (n = tab.length) == 0)  
		    // 数据桶为空,就先初始化
            tab = initTable();  
        // 定位到这个位置之后发现桶中没有数据,就直接放入,不加锁,然后跳出即可
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {  
            if (casTabAt(tab, i, null,  
                         new Node<K,V>(hash, key, value, null)))  
                break;                   // no lock when adding to empty bin  
        }  
        // 检查当前桶中的第一个节点是否是一个 ForwardingNode。ForwardingNode 是在哈希表扩容时使用的一种特殊节点类型,用于指示该桶的内容已经被移动到新的哈希表中。 
        // 如果是就需要helpTranfer
        else if ((fh = f.hash) == MOVED)  
            tab = helpTransfer(tab, f);  
        else {  
            V oldVal = null;  
            // 对节点f加锁
            synchronized (f) {  
                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;  
                            }                        }                    }                    else if (f instanceof TreeBin) {  
                            //发现是红黑树
                        Node<K,V> p;  
                        binCount = 2;  
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,  
                                                       value)) != null) {  
                            oldVal = p.val;  
                            if (!onlyIfAbsent)  
                                p.val = value;  
                        }                    }                }            }            if (binCount != 0) {  
                if (binCount >= TREEIFY_THRESHOLD)  
                    treeifyBin(tab, i);  
                if (oldVal != null)  
                    return oldVal;  
                break;  
            }        }    }    addCount(1L, binCount);  
    return null;  
}
  1. 通过key计算hashcode
  2. 判断是否需要初始化
  3. 定位到Node,如果为空直接用CAS写入,失败就自旋保证成功
  4. 如果hashcode == MOVED == - 1需要进行扩容
  5. 如果都不满足就使用synchronized锁写入数据
  6. 如果数量大于TREEIFY_THRESHOLD调用treeifyBin进行转化

get

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // key 所在的 hash 位置
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果指定位置元素存在,头结点hash值相同
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                // key hash 值相等,key值相同,直接返回元素 value
                return e.val;
        }
        else if (eh < 0)
            // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            // 是链表,遍历查找
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

常见八股

HashMap的底层实现是什么

JDK1.8之前是数组 + 链表,数组是HashMap的主体,链表是用来解决哈希冲突。使用拉链法来解决。
JDK1.8之后变为数组+链表/红黑树,当链表长度大于等于阈值 8 时,会转化,如果当前数组长度小于64就学会先进行数组扩容,否则就转化为红黑树,默认大小为16,每次扩容2倍。

拉链法到底是什么

拉链法就是使用一个链表数组,数组中的每一格都是一个链表,如果遇见哈希冲突,则将冲突的值加到链表中。
JDK1.8之后会先判断链表的长度是否大于阈值8,然后去根据数据来判断是否转化为红黑树。

|450

为什么使用2作为底数

  1. 使用2作为底数可以使用位运算而不是取模去计算索引,比较方便,提高性能
  2. 同时容量是2的幂次时,哈希值的低位和高位都可以参与索引的计算,减少哈希冲突(capacity 是 2 的幂次,因此 capacity - 1 的二进制表示全是 1,这样 hash & (capacity - 1) 就能高效地计算出索引。)
  3. 扩容更更简单,重新计算哈希只需要检查哈希值的一个额外位

讲解一下put的过程

  1. 判断map是否为空,如果为空或者长度位0就及逆行初始化或者扩容
  2. 之后计算hash值,去匹配Node数组,如果定位到的数组没有位置为空,则直接插入到Node数组中
  3. 如果不为空,判断是否是树节点,如果是就调用putTreeVal,否则就去遍历这个数组所对应的链表,如果出现哈希值和key完全一致就覆盖数据,否则就遍历到最后使用尾插法,插入数据
  4. 插入或者覆盖数据之后判断是否需要扩容或者转化为树形结构

为什么JDK1.8使用尾插法而不是和JDK1.7一样使用头插法

因为多线程的情况头插法有可能会出现环

HashMap何时会扩容

当链表的长度大于TREEIFY_THRESHOLD(默认为8)时会开始转化,同时要判断Node数组的个数是否大于MIN_TREEIFY_CAPACITY(默认64),如果小于就会先进行扩容,而不是转化,如果大于就直接转化为红黑树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值