[JavaSE 源码分析] 关于HashMap的个人理解

2023-05-16

目录

  • HashMap是什么?
  • HashMap的底层数据结构是什么?
    • table容量为什么必须是二的倍数?
    • table容量怎么做到二的倍数?
  • Entry是怎样的结构?
    • Node: Entry在HashMap中的具体实现
    • 处理hash冲突的方法
  • HashMap初始化或扩容 resize()
  • HashMap计算元素的hash
  • HashMap添加/更新元素
  • HashMap取值
  • HashMap删除元素
  • HashMap为什么是非线程安全的?
    • HashMap在并发场景下可能存在哪些问题?
  • 通过Debug来进一步理解HashMap
    • 环境
    • 过程
    • Debug技巧
  • 参考文章

HashMap是什么?

Map是Java常用的一种存储数据的Key-Value结构, 键值对
HashMap是Map结构, 底层采用Hash算法存取Key值

HashMap: 基于哈希表的 Map 接口的实现.

  1. 此实现提供所有可选的映射操作, 并允许使用 null 值和 null 键.
    (除了非同步和允许使用 null 之外, HashMap 类与 Hashtable 大致相同. )
  2. 此类不保证映射的顺序, 特别是它不保证该顺序恒久不变.
  3. 此实现假定哈希函数将元素适当地分布在各桶之间, 可为基本操作(get 和 put)提供稳定的性能.
  4. 迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例. 所以, 如果迭代性能很重要, 则不要将初始容量设置得太高(或将加载因子设置得太低)

HashMap的底层数据结构是什么?

Entry是Map的辅助数据结构, Map的底层结构是Entry数组
但处理hash冲突使用链表法, 使用链表的方式存储冲突元素
当某位置上的冲突数据累计到一定程度上时, 会转化成红黑树结构

// 必定是2的倍数 
transient Node<K,V>[] table;

HashMap的底层数据结构是数组和链表 用于存放元素
当某位置上元素个数大于等于8(并且table大小大于64)时 链表转换成红黑树结构 TREEIFY_THRESHOLD=8
if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash)
元素个数小于6时 红黑树结构转换成链表结构 UNTREEIFY_THRESHOLD=6
if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map)

数组的特点是: 寻址容易, 插入删除困难
链表的特点是: 寻址困难, 插入删除容易
红黑树: 自平衡的二叉查找树, 查找效率是非常的高, 查找效率会从链表的o(n)降低为o(logn)

  1. 构造红黑树要比构造链表复杂, 在链表的节点不多的时候, 从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高
  2. HashMap频繁的扩容, 会造成底部红黑树不断的进行拆分和重组, 这是非常耗时的.
    因此, 也就是链表长度比较长的时候转变成红黑树才会显著提高效率

1640888-20190925230551168-87583559.png

table容量为什么必须是二的倍数?

为了更方便的存取数据, 通过位运算代替模运算
hash&(n-1)就是它的存放位置

// index 为元素在table数组中存放位置
// n = table.length
// hash 为key的hash
index = (n - 1) & hash;

// 有可能1都集中在前16位中 
// 而导致明明相差很大的数据 因为后16位相同而发生冲突
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 右位移16位, 正好是32bit的一半, 自己的高半区和低半区做异或, 
// 就是为了混合原始哈希码的高位和低位, 以此来加大低位的随机性. 
// 而且混合后的低位掺杂了高位的部分特征, 这样高位的信息也被变相保留下来. 

table容量怎么做到二的倍数?

通过右移和或位运算的方式 得到大于等于输入参数的最小2的倍数

/**
* 返回大于等于给定参数的值(2的倍数)
* 首位为1 其余为0
* cap最大为: 1 << 30
*
* 先求全1 再加1 --> 1111 + 1 = 1 0000
*/
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
  • 为什么要对cap做减1操作?
    这是为了防止, cap已经是2的幂. 如果cap已经是2的幂, 又没有执行这个减1操作, 则执行完后面的几条无符号右移操作之后, 返回的capacity将是这个cap的2倍.
  • 因为输入参数不为0 则必有1bit为1 最高位为1
    第一次右移: 最高位和次高位 最高位必为1
    第二次右移: 前两位和前三四位 前两位必为1
    第三次右移: 前四位和前5~8位 前四位必为1
    ...
    最高位以下全为1
  • 最终该值被赋予threshold 只是临时存储 在resize()初始化时
// initial capacity was placed in threshold
else if (oldThr > 0) 
    newCap = oldThr;

Entry是怎样的结构?

Entry是单个键值对, 提供简单的key, value的操作方式

interface Entry<K,V> {
    // 返回该实例存储的Key
    K getKey();

    // 返回该实例存储的Value
    V getValue();

    // 替换该实例存储的value 
    // 返回原有value值
    V setValue(V value);

    // 判断两实例相等的方法
    // 一般指定两者的Key, Value均要相等
    boolean equals(Object o);

    // 获取该实例的hashCode
    // 一般为该实例的唯一标识
    int hashCode();

    // 返回一个比较Entry key值的比较器
    public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
        return (Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> c1.getKey().compareTo(c2.getKey());
    }

    // 返回一个比较Entry value值的比较器
    public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
        return (Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> c1.getValue().compareTo(c2.getValue());
    }

    // 通过给进比较key值的比较器 来获得一个比较Entry的比较器
    public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
        Objects.requireNonNull(cmp);
        return (Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
    }

    // 通过给进比较value值的比较器 来获得一个比较Entry的比较器
    public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
        Objects.requireNonNull(cmp);
        return (Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
    }
}

Node: Entry在HashMap中的具体实现

Node是个链表节点结构, 主要用途在处理hash冲突时, 作为缓解手段
(计算hash得出的存储位置上已经有元素, 则将该元素作为上一元素的下一节点)

static class Node<K,V> implements Map.Entry<K,V> {
    // hash, key一般赋值后不能被修改
    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; }

    // 该Node实例的hashCode是key的hashCode和value的hashCode相异或
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // key,value都相等
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

处理hash冲突的方法

  1. 开放定址法
    一旦发生了冲突, 就去寻找下一个空的散列地址, 只要散列表足够大, 空的散列地址总能找到, 并将记录存入
  2. 链地址法
    将哈希表的每个单元作为链表的头结点, 所有哈希地址为index的元素构成一个同义词链表.
    即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部
  3. 再哈希法
    当哈希地址发生冲突用其他的函数计算另一个哈希函数地址, 直到冲突不再产生为止
  4. 建立公共溢出区
    将哈希表分为基本表和溢出表两部分, 发生冲突的元素都放入溢出表中

HashMap初始化或扩容 resize()

  1. HashMap直到调用它的时候才开始初始化, 在添加数据后才会开始判断是否需要扩容(通过阈值)
  2. 如果没有设置参数 map容量为默认的16 阈值为9 (在此时设置)
  3. 扩容时, 容量和阈值均翻倍
/**
* 初始化或数组容量翻倍
*/
final Node<K, V>[] resize() {
    // 获得原有全局表 table
    Node<K, V>[] oldTab = table;
    // 获得原有表的容量 
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 获得原有表的阈值 容量*负载因子
    // 如果设置了初始容量 threshold等于设置的初始容量(大于等于输入参数的最小的二倍数)
    // 如果没有则为0
    int oldThr = threshold;
    // 设置新表的容量和阈值
    int newCap, newThr = 0;
    // 如果原有表不为空 
    if (oldCap > 0) {
        // 如果原有表的容量大于等于最大容量 不用扩展
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        // 如果旧表的容量 大于16 翻倍后小于最大容量
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新表的阈值也是旧表阈值的两倍
            newThr = oldThr << 1; 
    // 如果旧表为空 并且事先设置了参数 threshold不为空
    } else if (oldThr > 0) 
        // 使用初始化的旧表阈值做新表的容量
        newCap = oldThr;
    // 旧表为空 没有设置参数 threshold为空
    else {
        // 新表容量 使用默认初始容量 16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新表阈值为 16*0.75 = 12
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 以上就处理完 新表的参数 容量newCap
    // 此时 旧表为空 并且设置了参数 threshold不为空
    if (newThr == 0) {
        // 使用新表的容量计算新表的阈值
        float ft = (float) newCap * loadFactor;
        // 新表的容量小于最大容量 计算的新表阈值也小于最大容量 则获得新表阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    // 以上就处理完 新表的参数 容量newCap和阈值newThr
    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;
                // 如果该位置只有一个Node 没有下一Node
                if (e.next == null)
                    // 通过indexFor存入新表中
                    newTab[e.hash & (newCap - 1)] = e;
                
                // 判断该位置Node的链接Node是什么结构?
                // 树形结构 红黑树
                else if (e instanceof TreeNode)
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                // 链状结构 链表
                else {
                    // head 指向链表头部 tail 构建整个链表
                    Node<K, V> loHead = null, loTail = null;
                    Node<K, V> hiHead = null, hiTail = null;
                    Node<K, V> next;
                    do {
                        // 先取到该位置的下一节点
                        next = e.next;
                        // e.hash & oldCap = 0 则该Node不需要移位
                        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;
}

1640888-20190925230633802-420269225.png

HashMap计算元素的hash

hashCode和自己高16位相异或
如此使得hash更离散 使得元素分布的更均匀
一般我们的容量都比较小, 2^16
而计算元素key值位置是hash&(n-1) 基本只有低几位信息是有效的 如此的hash冲突比较大
如果将高位的信息也参与运算 可以一定程度上缓解冲突

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

设计者将key的哈希值的高位也做了运算(与高16位做异或运算, 使得在做&运算时, 此时的低位实际上是高位与低位的结合), 这就增加了随机性, 减少了碰撞冲突的可能性

HashMap添加/更新元素

  1. 先检查table是否被初始化 没有则先resize() 初始化
  2. 通过hash&(n-1)获取存放位置 如果该位置为空 则存储元素
  3. 发生hash冲突, 查找该位置的key值相等的元素 没有则添加元素到尾部/叶子节点
  4. 查找到key值相等的元素 更新其value值, 返回旧值
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
    if ((tab = table) == null || (n = tab.length) == 0)
        // 初始化表格
        n = (tab = resize()).length;
    // i = (n - 1) & hash 通过位运算得到需要存放的位置
    // 如果该位置为空 则直接存储
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 发生hash冲突
    else {
        // 获取存放位置的元素
        Node<K,V> e; 
        K k;
        // p 是已存放元素
        // 判断p是否与将要存放的元素key相等
        // 如果key相等 表示是更新元素
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // key不相等 并且p为树状结构节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // key不相等 并且p为链状结构节点
        else {
            for (int binCount = 0; ; ++binCount) {
                // 找到最后一个节点
                if ((e = p.next) == null) {
                    // 添加节点
                    p.next = newNode(hash, key, value, null);
                    // 如果该链表长度大于等于7 则转化为红黑树结构
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 继续判断查询节点是否与将要存放的元素key相等
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果存放的位置不为空
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果表格元素个数大于阈值 则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

1640888-20190925230730380-382551580.png

HashMap取值

  1. 判断table是否为空 为空则返回null
  2. 检查hash&(n-1)的位置是否为空 为空则返回null
  3. 判断首位元素是否符合要求 满足则返回该元素
  4. 发生hash冲突, 查看该位置其他节点 找到就返回该元素 没有则返回null
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 
    Node<K,V> first, e; 
    int n; 
    K k;
    // 当table不为空 并且hash位置上存有元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 检查该位置的第一个元素
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果第一个元素 不符合查询条件
        // 则表示有可能是hash冲突 
        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);
        }
    }
    return null;
}

HashMap删除元素

  1. 判断table是否为空 为空则返回null
  2. 同getNode(hash, key)一样过程 先根据key值查找符合条件的元素
  3. 找到该元素再将其删除, 并返回该元素
public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}

final Node<K,V> removeNode(int hash, Object key, Object value, 
                            boolean matchValue, boolean movable) {
    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, index;
    // 判断table是否为空 并且hash位置上存有元素
    if ((tab = table) != null && (n = tab.length) > 0 && 
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; 
        K k; 
        V v;
        // 以下为找出符合要求的元素 根据key值 "getNode(hash, key)"
        // 首位元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 发生hash冲突 该位置其他节点
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 找打符合要求的元素node
        // matchValue 表示是否需要匹配value值 false表示不用 即使输入value不对 也可以删除该元素
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

HashMap为什么是非线程安全的?

因为里面的方法都不是线程安全的

HashMap在并发场景下可能存在哪些问题?

数据丢失 数据重复 死循环

  • Doug Lea writes:

通过上面Java7中的源码分析一下为什么会出现数据丢失, 如果有两条线程同时执行到这条语句 table[i]=null 时两个线程都会区创建Entry,这样存入会出现数据丢失.

如果有两个线程同时发现自己都key不存在, 而这两个线程的key实际是相同的, 在向链表中写入的时候第一线程将e设置为了自己的Entry,而第二个线程执行到了e.next, 此时拿到的是最后一个节点, 依然会将自己持有是数据插入到链表中, 这样就出现了数据重复.

通过put源码可以发现, 是先将数据写入到map中, 再根据元素到个数再决定是否做resize.
在resize过程中还会出现一个更为诡异的问题死循环.

这个原因主要是因为hashMap在resize过程中对链表进行了一次倒序处理.
假设两个线程同时进行resize, A->B 第一线程在处理过程中比较慢, 第二个线程已经完成了倒序编程了B-A 那么就出现了循环, B->A->B.这样就出现了就会出现CPU使用率飙升.

PS:在这个过程中可以发现, 之所以出现死循环, 主要还是在于对于链表对倒序处理, 在Java 8中, 已经不在使用倒序列表, 死循环问题得到了极大改善.

通过Debug来进一步理解HashMap

环境

  • IntelliJ IDEA 2018 专业版
  • Deepin Linux 15.9桌面版

过程

测试代码

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;

/**
 * 通过debug查看HashMap存储数据时的结构
 *
 * @author lingsh
 * @version 1.0
 * @date 19-9-25 下午4:29
 */

public class TestHashMap {
    public static void main(String[] args) {
        // size 存放数据量
        // cap HashMap初始设置容量 10 --> 16
        int size = 10000, cap = 10;
        Map<THMString, Integer> map = new HashMap<>(cap);
        // 存放数据
        for (int i = 0; i < size; i++) {
            map.put(THMString.getRandomString(), i);
        }
        // 用于定点调试(只是该main函数的终止位置, 可以用于暂停查看map结构)
        System.out.println(map.size());
    }
}

/**
 * TestHashMapString 
 * hashCode分布极其集中的自定义类
 * 底层是LEN大小的字符串
 */
class THMString {
    /**
     * 底层真实数据
     */
    private String str;
    /**
     * SIZE 用于加强实例碰撞的可能性
     */
    private final static int SIZE = 1024;
    /**
     * 底层数据大小
     */
    private final static int LEN = 5;
    /**
     * 使用随机数来确定底层数据内容
     */
    private static Random random = new Random();

    public THMString(String str) {
        this.str = str;
    }

    @Override
    public int hashCode() {
        // 集中hashCode 通过不断的取余来加强碰撞可能
        return str.hashCode() % SIZE % SIZE % SIZE % SIZE % SIZE;
    }

    @Override
    public boolean equals(Object obj) {
        THMString thms = (THMString) obj;
        return Objects.equals(this.str, thms.str);
    }

    @Override
    public String toString() {
        return "[String:" + str + "\thashCode:" + hashCode() + "]";
    }

    /**
     * 获取随机的实例作为HashMap的Key
     * 
     * @return 随机的实例
     */
    static THMString getRandomString() {
        char[] chars = new char[LEN];
        for (int i = 0; i < chars.length; i++) {
            int word = random.nextInt('z' - 'a');
            chars[i] = (char) (word + 'a');
        }
        return new THMString(new String(chars));
    }
}

目标成果

底层数据结构
  • Node
    1640888-20190925225402835-922633124.png
  • TreeNode
    1640888-20190925225411514-964857138.png
扩容过程
  • 扩容开始时
    1640888-20190925225428541-1328881559.png

  • 扩容时移动一个节点到新表
    1640888-20190925225437844-1208571918.png

Debug技巧

关闭Debug时IDEA优化的类结构

1640888-20190925225445182-1957658513.png

当开启该选项时, IDEA将参数隐藏, 比如:next, newThr等

发现table存放jar路径

1640888-20190925225452822-2072481440.png

因为在源码debug时, 不仅我们个人在使用HashMap, 运行程序系统也会使用到它, 所以我们可能会捕捉到系统在初始化时调用HashMap存取值
所以, 建议

  • 先给自己程序设断点, 确定在开始自己的程序, 再到HashMap源码那设置断点

参考文章

  1. HashMap完全解读
  2. HashMap之resize详解
  3. HashMap源码注解 之 静态工具方法hash()、tableSizeFor()(四)
  4. HashMap就是这么简单【源码剖析】
  5. HashMap源码分析(jdk1.8, 保证你能看懂)
  6. hashmap的线程不安全体现在哪里?
  7. idea调试debug(HashMap,ArrayList等)开启/关闭集合类视图

转载于:https://www.cnblogs.com/slowbirdoflsh/p/11585463.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

[JavaSE 源码分析] 关于HashMap的个人理解 的相关文章

随机推荐

  • 相册列表 鼠标悬停显示照片介绍

    lt DOCTYPE HTML PUBLIC 34 W3C DTD HTML 4 01 Transitional EN 34 34 http www w3 org TR html4 loose dtd 34 gt lt html gt lt
  • 图书管理系统(毕业论文)

    毕 业 设 计 论 文 题 目 xff1a 图书管理系统 院 系 xff1a 计算机学院 专 业 xff1a 软件技术 姓 名 xff1a XXX 指导教师 xff1a XX 2017年 10 月 23 日 1 引言 5 2 相关技术突破
  • C#中定义数组--字符串及数组操作

    一 一维 xff1a int numbers 61 new int 1 2 3 4 5 6 不定长 int numbers 61 new int 3 1 2 3 定长 二 多维 int numbers 61 new int 1 2 3 1
  • 迭代器分类

    输入迭代器 读 xff0c 不能写 xff1b 只支持自增运算 istream iterator 61 61 61 43 43 gt 输出迭代器 写 xff0c 不能读 xff1b 只支持自增运算 ostream iterator 43 4
  • VC++中隐藏代码

    1 引言 在VS编辑器中可以对类中的方法 注释等内容进行隐藏 xff0c 单击左侧的 号即可完成隐藏 xff0c 隐藏后变为 43 xff0c 单击 43 号可以将隐藏的代码展开 2 隐藏任意代码 如果想在编辑器中隐藏任意代码段 xff0c
  • 常见签名算法之SHA256withRSA

    概述 在https blog csdn net chinoukin article details 100934995章节中 xff0c 我介绍了用Hmac算法用于签名算法中的方法 xff0c 本章节中将对常见的签名算法 SHA256wit
  • httpclient4封装类与HttpParser封装类

    httpclient4封装类与HttpParser封装类 最近工作中需要做一个爬虫去抓取指定页面的一些内容 xff0c 准备使用HttpParser来解析页面结构 xff0c 顺便看了一下httpclient4 xff0c 可以将它们配合使
  • 【Linux操作系统分析】进程——进程切换,进程的创建和撤销

    1 进程 进程是程序执行时的一个实例 xff0c 可以把它看作充分描述程序已经执行到何种程度的数据结构的汇集 从内核的观点看 xff0c 进程的目的是担当分配系统资源 xff08 CPU时间 xff0c 内存等 xff09 的实体 xff0
  • C++中的.和::和:和->的区别

    在学习C 43 43 的过程中我们经常会用到 和 和 xff1a 和 gt xff0c 在此整理一下这些常用符号的区别 1 A B则A为对象或者结构体 xff1b 2 A gt B则A为指针 xff0c gt 是成员提取 xff0c A g
  • 通过Curl 对url进行encode操作

    最近做项目的时候 xff0c 通过 Gflags Reload 时候 发现对于某些value中包含 61 中文等字符的支持不够好 xff0c value被截断了 经过分析后 xff0c 发现程序对url切分是用 61 amp 为标准的 xf
  • STM32进阶之串口环形缓冲区实现(转载)

    转载自微信公众号 玩转单片机 xff0c 感谢原作者 杰杰 队列的概念 在此之前 xff0c 我们来回顾一下队列的基本概念 xff1a 队列 Queue xff1a 是一种先进先出 First In First Out 简称 FIFO 的线
  • 位和结构体寄存器访问方法(转)

    1 2 1 传统 define 方法 1 2 外设位域结构体方法综述 DSP281x 头文件和外设示例使用位域结构体方法 xff0c 映射和访问基于F28x 外设寄存器 本节将介绍这种方法 xff0c 并把它和传统的 define 方法加以
  • 关于将函数写入头文件问题(分离式编译)

    如果某些函数在其他很多 cpp 文件中被调用 xff0c 那么为了避免写大量重复的代码以及让代码量更小一些 xff0c 我们可以将这些函数写头文件中 xff0c 然后其他 cpp 文件只需要引用该头文件然后就可以使用包含在头文件中的函数了
  • SpringSecurity配置跨域访问

    说明 java后端web服务有很多种方法可以实现跨域访问 xff0c 配置很简单 xff0c 今天这里我们用SpringSecurity的方式配置跨域访问 xff0c 配置方法如下 xff1a span class token keywor
  • 嵌入式C语言开发---存储器与寄存器

    概述 xff1a 讲述如何使用C语言来对底层寄存器进行封装 内容 xff1a 存储器映射寄存器与寄存器映射C语言访问寄存器 存储器映射 程序存储器 数据存储器 寄存器和I O 端口排列在同一个顺序的4 GB 地 址空间内 存储器映射 xff
  • httplib用法

    httplib的内容上是很多 xff0c 也比较简单 以下是一个非常简单的例子 xff0c 使用httplib获取google首页的html xff1a import httplib conn 61 httplib HTTPConnecti
  • HTTP认证之摘要认证——Digest(一)

    导航 HTTP认证之基本认证 Basic xff08 一 xff09 HTTP认证之基本认证 Basic xff08 二 xff09 HTTP认证之摘要认证 Digest xff08 一 xff09 HTTP认证之摘要认证 Digest x
  • Linux 文件名和路径的最大长度

    在x86 64 Linux下 xff0c 文件名的最大长度是255个字符 characters xff0c 文件路径的最大长度是4096字符 characters xff0c 即可以包含16级的最大文件长度的路径 在 lt limits h
  • Django之auth

    一 xff1a auth基础 xff08 1 xff09 作用 xff1a django提供给开发人员 对用户进行操作的模块的 例如 xff1a 登录 注册 认证 注销等等 xff08 2 xff09 使用方式 from django co
  • [JavaSE 源码分析] 关于HashMap的个人理解

    目录 HashMap是什么 HashMap的底层数据结构是什么 table容量为什么必须是二的倍数 table容量怎么做到二的倍数 Entry是怎样的结构 Node Entry在HashMap中的具体实现处理hash冲突的方法HashMap