- 主要是在 javaGuide 以及 CYC2018 的基础上做了修改以及补充。
- 其它参考资料都在文末给出,建议阅读。
- ⭐️内容较多,点赞收藏不迷路 ⭐️
i++ 是先使用,后自增。
++i 是先自增,后使用。
自增与使用两个操作不是原子性的,所以在多并发环境下会出现问题,比如值覆盖。
List 和 Set 都是继承自 Collection。
List 可以有多个元素引用相同的对象,Set 不允许重复的集合。不会有多个元素引用相同的对象。
Map 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String、Integer类型,但也可以是任何对象。
Set
List
Map
Vector
类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist
不是同步的,所以在不需要保证线程安全时建议使用Arraylist。
size + 1
,然后调用 ensureCapacityInternal
函数确保有足够的容量使插入成功。ensureCapacityInternal
中,会先判断当前的数组容量是否是默认数组,如果是的话就取 max(默认数组大小,传入的指定最小大小)。ensureExplicitCapacity
确定具体大小,在这之前会让 modCount ++
,以此来支持实现 fail-fast
,再判断最小值是否大于了数组长度,如果是的话就实施扩容。grow
来实现扩容,oldCapacity + (oldCapacity >> 1)
新数组长度是旧数组长度的 1.5 倍。synchronized
修饰。多线程下一般使用 ConcurrentHashMap
;HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()
、writeObject()
、readObject()
是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
ashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用 put() 向map中添加元素 |
调用 add() 方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, |
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,长度小于8时会退化成链表)时,将链表转化为红黑树,以减少搜索时间。
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
两个线程同时需要扩容,扩容下 transfer() 转移部分容易出现死循环。
线程一已经扩容完毕,此时线程一的新 HashMap 的链表是倒序的。线程二开始扩容,但是它的初始指针指向的内容以及顺序还是旧链表的,但是此时这些东西已经到了线程一创建的新链表,所以后面线程二会去改变线程一的新链表结构,按照头插法的特点,最后线程二形成的链表会形成一个死循环,而且可能会造成数据丢失。
当执行 get 时,当key 正好被分到那个 table[i] 上时,遍历链表就会产生循环。
因此多线程情况下建议使用ConcurrentHashMap。
循环的产生是因为新链表的顺序跟旧的链表是完全相反的,所以只要保证建新链时还是按照原来的顺序的话就不会产生循环。
JDK8是用 head 和 tail 来保证链表的顺序和之前一样,这样就不会产生循环引用。但 1.8 还是没有解决数据丢失的问题。
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
1.8
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
compareTo(Object obj)
方法用来排序compare(Object obj1, Object obj2)
方法用来排序 Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
1. List
2. Set
主要根据集合的特点来选用。
比如我们需要根据键值获取到元素值时,就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
哈希码的范围比 HashMap 的范围大,容易出现范围不匹配。
2.3 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题。
2.4 为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
hold
哈希码的范围比 HashMap 的范围大,容易出现范围不匹配。
2.3 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题。
2.4 为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突。
《有备而来——Java基础面试题全总结》
《Java 热点基础》
参考资料
《HashMap 多线程下死循环分析及JDK8修复》
《Java 8系列之重新认识HashMap》
《老生常谈,HashMap的死循环》
《Java:手把手带你源码分析 HashMap 1.7》
⭐️ 如果对你有帮助,麻烦点个