本文通过探索HashMap的使用和存在的问题,循序渐进的探索和引入各种原理和编程思路。主要内容包括:
-
- 什么是HashMap
- HashMap如何扩容
- 为什么HashMap的初始化容量默认为16
- 负载因子为什么是0.75
- hash方法的具体实现
- LinkedHashMap实现LRU缓存
- HashMap存在的问题及解决办法
-
- 分段锁
- CAS+Synchronized
- 初始化构造原理
探索HashMap
-
1、【什么是HashMap】
- 数据结构:HashMap是一个数组加链表的数据结构(链地址法)。
- 使用:加入数据首先经过hash,放入计算出的位置,如果该位置有值,即发生hash冲突,一般hash冲突有四种处理方式:
- 链地址法:该地址存放的是一个链表,直接在链表尾端加入数据即可。
- 开放定址法:顺延下一个不冲突的地址。
- rehash:使用另一种hash算法
- 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
-
2、【HashMap如何扩容】
- HashMap里面有数组,数组上挂的是链表,数组有个设定值,链表也有个扩容长度,当链表长度大于8,进一步判断数组长度,
- 数组长度小于设定值MIN_TREEIFY_CAPACITY(64),只是对数组进行扩容,把已存数据进行再次hash处理。
- 每次扩容,数组长度增加一倍。
- 如果预先知道要存储1000个数,设置多大的 HashMap比较合理:1000<0.75x,求出x是1334,new HashMap(1334),自动会把存储大小改成大于1334的最小2的次方:2048。
- 数组长度超过64时,将链表转换成红黑树。
- 参考:HashMap中的链表什么时候转化为红黑树
-
2.1、【为什么HashMap的初始化容量默认为16】
- 为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
- X % 2^n = X & (2^n – 1)
- 选16是经验值
- 如何找到比传入值大的最小2的次方:数学问题,不会。
- 为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
// 找到第一个大于等于initialCapacity的2的平方的数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
-
2.2、【负载因子为什么是0.75】
- 因为综合考虑时间复杂度和空间复杂度,折中选择0.75.
-
2.3、【hash方法的具体实现】
static int indexFor(int h, int length) {
return h & (length - 1);
}
-
为什么是length - 1
- 因为length一般是2的次方,会导致奇数位的数据hash不到。
-
3、【LinkedHashMap实现LRU缓存】
- LinkedHashMap支持两种顺序插入顺序 、 访问顺序
- 插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序
- 访问顺序:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。
- 参考: LinkedHashMap基本用法&使用实现简单缓存
- LinkedHashMap支持两种顺序插入顺序 、 访问顺序
-
4、【多线程情况下HashMap存在的问题及解决办法】
- 4.1、 存在的问题:多线程环境下,
- (1)多线程操作HashMap会引起脏数据。
- (2)使用Hashmap进行put操作可能会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。JDK1.8前多线程并发下HashMap会发生死循环
- 4.2、解决办法
- 使用Hashtable,使用synchronized来保证线程安全。
- 但是使用HashTable会引起效率问题,同一把锁效率太低。
- 于是引入ConcurrentHashMap,在jdk1.8之前使用分段锁,不同的segment是不同的锁,但是即使这样仍然会减小效率。
- 在jdk1.8之后,ConcurrentHashMap引入了无锁模式。
- 4.1、 存在的问题:多线程环境下,
探究ConcurrentHashMap
- 分段锁(1.8以前)
- ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock
- CAS+Synchronized(1.8)
- 初始化构造原理
//返回一个大于输入参数且最小的为2的n次幂的数。
private static final int tableSizeFor(int c) {
int n = c - 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;
}
//tableSizeFor(int c)的原理:
//将c最高位以下通过|=运算全部变成1,最后返回的时候,返回n+1;