hm的数据结构是数组加链表,插入数据的话,先对数据计算一个哈希值,然后用这个哈希值去对数组的大小减1来取模,算出来的数就是存在数组里的位置,如果这个位置已经有数据了,这就是哈希碰撞问题,hm会用equls方法对这个数据的值和数组里的值进行比较,如果这个数据已经做了链表,就挨个比较,只有equls结果全为false,才会进行插入,1.8之前呢就是直接用一个链表来保存这种碰撞的数据,jdk1.8做了优化,当链表长度超过8的时候就会升级为红黑树,时间复杂度为O(logn),这个就是hm的数据结构的情况。
jdk还对hashmap的寻址算法进行了优化,先看1.8求hash值得源码
这里用哈希值和它自己右移16位的值进行异或运算的原因是,因为数组大小前16位一般都是0,这样hash和数组大小进行寻址计算的时候前16位结果都是0,相当于前16位没有参与运算,这样会增大hash冲突的概率,这里做一下异或处理就可以让hash前16位也参与寻址运算,减小冲突概率。jdk1.8使用(n-1)&hash计算出数组的一个位置来代替之前的hash%(n-1),因为与运算与取模的结果一致,但是前者性能比后者强。
hashmap的容量默认是16
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
hashmap定义了一个加载因子,它的默认值是0.75
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容。扩容的方法是创建一个新table,大小和原来一样,再合并起来,这样扩容之后大小变为两倍。
1.8以前采用的是头插法来扩容,这样在多个线程同时进行插入,触发扩容机制,就有可能形成环状节点,导致死循环。
1.8之后的扩容方式是创建一个新table,长度是原来的两倍,然后循环把原来的元素放入新table中,这里会重新对每个hash寻址,称作rehash,它计算的结果是要么和之前一样,要么等于之前的值加上旧数组的长度。
前面提到了hashmap会有线程安全问题,解决线程安全的问题一般有三种,用Synchronized或者Reentrantlock加锁,还可以采用ConcurrentHashMap。
ConcurrentHashMap是一种线程安全的hashmap,它的内部采用分段锁来实现,在jdk1.7采用的segment继承了ReentrantLock实现加锁的功能,一个segment里面有一个小的hashmap,而在1.8中采用的synchronized+CAS保证线程安全,没有使用分段锁。
一般会继续追问并发的一些知识点,比如Synchronized的原理,CAS,AQS