一、线程不安全的Map
HashMap
获取entry数组下标的方式:按位与
- 根据key获得一个hashValue[注:hashValue=hash(key)],然后用hashValue对length-1进行按位与运算得到数组的下标,即:hashValue&(length-1)
- 数组的length必须是2的整数次幂,原因如下:
- 第一:若length是2的整数次幂,则hashValue&(length-1)等价于hashValue%length,那么hashValue&(length-1)同样也实现了均匀散列,但是(位运算)效率会更高。
1>归纳: 2^1 -1 = 0000 0001 2^2 -1 = 0000 0011 2^3 -1 = 0000 0111 2^n -1 = 0000 (n个1) 2>举例: 若:hashValue=29,length=16 则:hashValue & (length -1) ==> 29 & (2^4-1) ==> 00011101 & 00001111 = 00001101 ==> 13 ==> 0<= hashValue & (length -1) <=length -1 hashValue % length ==> 29 % 16 ==> 13 ==> 0<= hashValue % length <=length -1 故:hashValue & (length -1) == hashValue % length 3>结论:当length=2^n时,hashValue & (length -1) == hashValue % length,且二者的结果范围都是:0到length-1之间的整数。
- 第二:若length为奇数,则length-1为偶数,偶数(二进制)的最后一位是0,从而导致hashValue&(length-1)的最后一位永远为0,即:hashValue&(length-1)的结果永远为偶数,最终导致数组中下标为奇数的空间全部被浪费掉。
- 第一:若length是2的整数次幂,则hashValue&(length-1)等价于hashValue%length,那么hashValue&(length-1)同样也实现了均匀散列,但是(位运算)效率会更高。
代码:
int hash = hash(key);
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
return h & (length-1);
}
HashMap在jdk8中的优化:
- 数据结构:
- jdk7:entry数组,entry是一个单向链表。
- jdk8:entry数组,entry是一个 单向链表 或 红黑树。
- 扩容:
- jdk7:初始容量16,容量到达阈值后进行扩容,每次扩容后,容量变为之前的2倍。
- jdk8:
- 初始容量16,容量到达阈值后进行扩容,每次扩容后,容量变为之前的2倍。
- 当链表长度大于等于8且当前容量还没达到最小树化容量(64)时,会进行扩容以减少冲突。这个逻辑在树化方法中:java.util.HashMap#treeifyBin
- 说明:
- 刚开始时HashMap的容量较小,故哈希碰撞的几率会比较大一些,即出现长链表的可能性会稍微大一些。因为容量较小而产生的长链表,我们应该优先选择扩容来降低冲突,而不是树化。
- eg:可能存在多个长链表,一次扩容可以同时降低这些链表的长度,若选择树化来降低冲突,则需要操作n次。
- jdk8的树化(将链表转化为红黑树)和去树化:
- 树化:当容量超过最小树化容量(64)时,如果存在链表长度大于等于树化阈值(8)时就会树化,故最坏的情况下的查找时间复杂度为O(logN)。jdk7最坏的情况下会遍历整个链表,时间复杂度为O(N)。
- 去树化:扩容的时候,若发现红黑树中节点的数量小于等于去树化阈值(6)时,会将红黑树转换为链表。
- 说明:
- 若将去树化的阈值也设计为7,则当一个HashMap不停的 插入元素,再删除元素 时就会导致不断地进行树化和去树化的操作,导致效率降低。
- TreeNodes占用的空间是普通Node的两倍,这样设计是为了追求时间和空间的平衡。
- 线程安全:
- jdk7:线程不安全,多个线程同时扩容时,可能会生成循环链表,导致cpu飙高。
- jdk8:线程不安全,不会生成循环链表,但是put操作时存在丢失数据的情况。
- 链表插入新节点的方式:
- jdk7:新节点添加到头部
- jdk8:新节点添加到尾部
TreeMap
- 数据结构:基于红黑树实现。
- 特点:键值对默认根据key升序排序,也可以自己定义比较器。
二、线程安全的Map
ConcurrentHashMap
HashTable
获取数组下标的方式:取模法
- 根据key获得一个hashValue[注:hashValue=hash(key)&0x7FFFFFFF],然后用hashValue对数组的长度取模得到数组的下标,即:hashValue%length
- 取模法基本能保证元素在哈希表中散列的比较均匀,但是取模会用到除法运算,效率很低。
代码:
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
HashMap VS HashTable
不同点:
- HashMap多线程下是不安全的,HashTable是线程安全的。
- HashMap的key和value都允许为null,HashTable的key和value都不允许为null(key或value为null时会抛出空指针异常)。
- HashMap的默认容量是16,扩容后的容量是之前的2倍;HashTable的默认容量是11,扩容后的容量是之前的2倍+1。
- 获取bucket的方式不同:HashTable通过取模法来获取bucket的下标, HashMap通过 按位与 来获取bucket的下标。
相同点:
- 都是Map的子类。
- 都是基于Entry数组实现的。