HashMap深入浅出

HashMap数据结构

  HashMap的本质就是一个数组加链表,数组默认长度是16,存储的元素达到总长度的75%就会扩容一倍。map.put(key,val),实际上就是根据hash散列对数组长度取模,来均匀的打到每一个下标上,填满数组每个下标位。但世事不可能这么完美,可能两个元素经过hash取模后下标会一样,为了避免hash冲突,hashmap就维护了一个链接的数据结构,相同下标的元素存到一个链表中。但是这样get(key)的时候会有一个问题,如果仅仅只是get数组上的元素速度会很快,但是get链表上的元素就会非常耗时,假使链表的深度为n,那么get所需的时间复杂度就是O(N),所以jdk1.8的时候做了一个优化,就是在原有的数据结构中加了一个红黑树,当链表的长度>=8时,会转成红黑树。

  

为何初始容量要是2的整数次幂

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

  这是jdk8中的初始容量,同时告诉我们默认的初始容量-必须是2的幂。可是我们创建  HashMap<String,String> map = new HashMap<String,String>(7); 也不会报错啊。实际上在初始化时,通过下面的代码它会初始化成一个大于我们值,且最接近它的一个2的幂次方。比如我们指定的7,那么它的初始值就是8,我们指定17那么初始值就是32。

/**
 * Returns a power of two size for the given target capacity.
 */
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;
}
    
那么问题就来了,为什么jdk8非要转一下呢?就用默认值难道不香吗?
    其实我们在put(key,value)的时候点进去开一下源码就知道了,它其实是通过位与运算来计算出下标的,这样的效率比取模更高。当然这和初始长度为什么是2的整次幂没关系,但如果长度不是2的整数次幂的话,位与运算和取模运算的出来的下标就会不一样,所以这也是jdk8的高明之处。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length - 1);
}

 HashMap的加载因子

    加载因子为什么是0.75?这里面其实就涉及到 时间复杂度和空间复杂度的平衡。因为loadfactor为1的话,这意味着一定要把整个数组填满才扩容,正常情况下是很难填满的,肯定会有很多hash碰撞,导致链表太长。如果loadfactor为0.5的话,数组填充一半就扩容,又会对空间利用率不高。所以取一个折中值0.75

JDK8对hashmap数据结构的优化

   /**
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)).
     The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     */
  当我们的链表长度达到8时,就会转换为红黑树的结构。源码的注释里也说了,这是根据"泊松分布"概率学得出来的一个结论,计算公式就是   (exp(-0.5) * pow(0.5, k) /* factorial(k)) ,而一个链表深度想要达到8的深度,它的概率只有0.00000006,所以这个红黑树对于整个hashmap数据结构的性能提升没有特别大。很多博客都说加载因子是为了满足做泊松分布,这其实是错误的,它们没有半毛钱的关系。

 HashMap线程安全问题

【1.】 put的时候,导致数据不一致问题。
1. 线程1计算好了桶的索引坐标,希望插入一个key-value对到HashMap中,但此时cpu时间片耗尽了,进入阻塞状态。
2. 线程2开始正在执行put操作,假使它的桶索引恰好和线程1是一样的,并数据插入成功。
3. 线程1现在被唤醒了再次运行,他还是持有的之前的链表头,继续往计算好的地方插入数据。
4. 最后就覆盖了线程2插入的记录,导致了线程2插入的记录就凭空消失了。

【2.】 数组扩容时的安全问题
扩容的时候,实际就是生成一个全新的数组,然后将以前的键值重新计算后写入新的数组。但多个线程同时检测到需要扩容时,最终只有一个线程赋值成功,其他的线程全都会丢失

 。

猜你喜欢

转载自www.cnblogs.com/wlwl/p/11954343.html