HashMap
参数
结构
数组 + 链表 Entry - Node
负载因子 0.75
初始大小 16
扩容大小 原始2倍
树化阈值 8 长度大于8的概率非常小
链表转树 6
MIN_TREEIFY_CAPACITY = 64 最小树化阈值,当Table所有元素超过改值,才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)
链表长度大于8且数组长度大于64
1.为什么用数组
链表也可以满足要求,数组的随机访问速度比LinkedList快,ArrayList的扩容机制是1.5倍,不方便扩容
2.为什么不直接使用红黑树
链表元素大于8的概率非常小,新增节点速度变慢了
6个元素退化为链表,防止频繁转换
2.如何计算hash
为了分散的更加均匀,与(length - 1) & hash值,例如开始长度是16,每次hash值和15也就是1111做与运算,可以得到桶的位置
3.put方法的过程
1.对key的hashCode()做hash运算,计算index; 2.如果没碰撞直接放到bucket⾥; 3.如果碰撞了,以链表的形式存在buckets后; 4.如果碰撞导致链表过⻓(⼤于等于TREEIFY_THRESHOLD),就把链表转换成红⿊树(JDK1.8中的改动); 5.如果节点已经存在就替换old value(保证key的唯⼀性) 6.如果bucket满了(超过load factor*current capacity),就要resize
4.resize
扩容为原始的两倍,并移动元素,每个元素要么在原始位置,要么在2倍位置
1.7头插法 死循环
1.8尾插法
5.get方法
1.对key的hashCode()做hash运算,计算index; 2.如果在bucket⾥的第⼀个节点⾥直接命中,则直接返回; 3.如果有冲突,则通过key.equals(k)去查找对应的Entry;
4.若为树,则在树中通过key.equals(k)查找,O(logn);
5.若为链表,则在链表中通过key.equals(k)查找,O(n)。
6.线程不安全
1.多线程扩容场景不安全 头插法 死循环
2.多线程put不安全,引起元素丢失
7.为什么不用hashtable,SynchronizedHashmap
全局使用sync锁,并发度很低
8.什么值作为key
⼀般⽤Integer、String这种不可变类当HashMap当key,⽽且String最为常⽤。 (1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。 这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。 这就是HashMap中的键往往都使⽤字符串。 (2)因为获取对象的时候要⽤到equals()和hashCode()⽅法,那么键对象正确的重写这两个⽅法是⾮常重要的,这些类已 经很规范的覆写了hashCode()以及equals()⽅法。
9.为啥Hashtable 不允许key和value为null
HashMap可以存放一个键是null,多个值是null 的对象,
而Hashtable则不可以存放键为null,或者是值为null的对象
而Hashtable则不可以存放键为null,或者是值为null的对象
为什么HashMap可以放null
专门做了处理当 key 为 null 时,HashMap 将会把 key-value pair放在第一个桶中,key == null时,hashcode返回为0
为什么HashTable不能存null键和null值?
原因:
- 当value值为null时主动抛出空指针异常
- 因为key值会进行哈希计算,如果为null的话,int hash = key.hashCode()时,还是会抛出空指针异常
fail-safe机制(modifyCount保证没有并发修改),直接做了检查防护 ,返回空指针异常
为什么ConcurrentHashmap不支持null
get(key)场景中,得到value为null,无法区分是value为空还是没有映射过
hashMap可以通过contains(key)方法来区分,ConcurrentHashmap的contains在并发场景下是不准确的,所以无法判断。
ConcurrentHashMap
hash在并发put常见不安全,即使1.8使用的是尾插法,不会死循环
Hashtable 和 SynchronizedMap一样,直接对get,set方法加锁,效率低
1.7 分段锁
原理
保存一个Segment数据,将hashmap分为多个段,每次对指定的段加锁,提高并发度,并发度就是段的个数,默认16
valitile修改每个桶,保证可见性
初始化
Segment 数组长度为 16,不可以扩容
Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
这里初始化了 segment[0],其他位置还是 null,其他的段在用到的时候才会初始化
put
找到对应的segment,如果没有初始化要先初始化,调用ensureSegment方法,为了防止多线程同时执行,使用CAS保证只有一个初始化成功
尝试获取写锁,首先自旋,到达一定次数后,改为阻塞锁获取,在segmeng内put
size
size为每一个segment的size之和,乐观锁的逻辑,如果计算size期间,没有put,remove等改变结构的操作,则直接计算
如果有put,就需要重试,重试3次都不行,就强制给所有segment加锁,再计算准确结果。
扩容
segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容,扩容后,容量为原来的 2 倍
每个segment自己扩容,不会影响其他段
get
先找到对应的segment,在正常取值
1.8 Synchronized + CAS
www.itqiankun.com/article/160…
原理
降低锁的粒度,提高锁的效率
将segment变为node,每次对当前链表或红黑树的头节点加锁
解决1.7中并发度低的问题,利于synchronized的优化
初始化
跟hashmap一致,提供了初始化容量,计算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1)
//sizeCtl 这个值有很多情况,默认值为0, //当为 -1 时,说明有其它线程正在对表进行初始化操作 //当表初始化成功后,又会把它设置为扩容阈值 //当为一个小于 -1 的负数,用来表示当前有几个线程正在帮助扩容(后边细讲)
put
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD则要转换为红黑树。
1.初始化表,初始化数组的时候,根据sizeCtl变量来保证只有一个数组来进行初始化
2.如果已经初始化,找到所在的桶
3.在put元素的时候,如果put的值没有发生hash冲突,此时(f = tabAt(tab, i = (n - 1) & hash)) == null中使用tabAt原子操作获取数组,并利用casTabAt(tab, i, null, new Node<K,V>(hash, key, value))CAS操作将元素插入到Hash表中
在存在hash冲突时,先把当前节点使用关键字synchronized加锁,然后再使用tabAt()原子操作判断下有没有线程对数组进行了修改,最后再进行其他操作。
4.使用synchronized加锁,将元素放入链表或红黑树
get
不需要加锁,get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
ConcurrentHashmap数组的volatile关键字 这里要注意ConcurrentHashmap数组上面也添加了volatile关键字,但是这个volatile关键字只是针对数组的地址,数组元素的值不是volatile的,而为了保证数组里面的元素也是volatile的,所以有了tabAt()和casTabAt()和setTabAt()这几个方法
这里添加volatile关键字的目的为了使得Node数组在扩容的时候对其他线程具有可见性而加的volatile
size
每次put元素成功以后,count元素自增1
正常我们使用volatile修饰变量,每次加1,这里为了提高在高竟态下的并发度,分散到不同对象里。