文章目录
-
- 1、有哪些常见的集合?
- 2、集合框架底层数据结构有哪些?
- 3、快速失败(fail-fast)和安全失败(fail-safe)了解吗?
- 4、ArrayList,LinkedList ,Vector的区别,包括存储结构,增删改查效率等。
- 5、HashMap的底层数据结构是什么?
- 6、怎么解决HashMap的hash冲突?
- 7、HashMap是线程安全的吗?怎么解决?
- 8、Hash的扩容策略是什么?
- 9、JDK1.8对HashMap主要做了哪些优化?为什么?
- 10、HashMap的put和get流程是什么?
- 11、ConcurrentHashMap底层具体实现?1.7和1.8有什么不同?
- 12、Set 和 Map的区别,List 和 Set 的区别?
1、有哪些常见的集合?
- Collection是集合List、Set的父接口
- List:存储的元素有序,可重复
- Set:不存的元素无序,不可重复
- Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的
- Map是另外的接口,是键值对映射结构的集合
2、集合框架底层数据结构有哪些?
List
- ArrayList:Object[] 数组
- Vector:Object[]数组
- LinkedList:双向链表
Set
- HashSet(无序、唯一):基于HashMap实现的,底层使用HashMap来保存元素
- LinkedHashSet:LinkedHashSet是HashSet的子类,并且其内部都是通过LinkedHashMap来实现的
- TreeSet(有序、唯一):红黑树
Queue
- PriorityQueue:Object[] 数组来实现二叉堆
- ArrayQueue:Object[] 数组+双指针
Map
- HashMap:JDK1.8之前
HashMap
由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。Jdk1.8之后当链表长度大于阈值8时,数组大于64时,将链表转换为红黑树 - LinkedHashMap:LinkedHashMap继承自HashMap,所以它的底层基于拉链式散列结构即由数组和链表或红黑树组成。此外在其基础上,增加了一条双向列表,使得上面的结构可以保持键值对的插入顺序,通过对链表的进行相应的操作,实现了访问顺序相关逻辑。java.util包下的
- HashTable:数组+链表组成的,数组是HashTable主体,链表是为了解决哈希冲突而存在的
- TreeMap:红黑树(自平衡的排序二叉树)
3、快速失败(fail-fast)和安全失败(fail-safe)了解吗?
- 快速失败:是Java集合中等的一种错误检测机制,当用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改,则会抛出
Concurrent Modification Exception
,原理是在遍历过程中使用一个modCount
变量,集合在被遍历期间如果内容发生变化,就会改变modCount,每当迭代下一个元素的时候,就会检测modCount
是否为expectionCount的值,是的话就遍历,不是的话就抛出异常。java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改。 - 安全失败:采用安全失败的机制的集合容器,在遍历时不是直接在集合内容上遍历,而是复制原先的集合内容,在拷贝的集合上进行遍历,原理是迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合的修改并不能被迭代器检测到,就不会触发
Concurrent Modification Exception
,java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
4、ArrayList,LinkedList ,Vector的区别,包括存储结构,增删改查效率等。
ArrayList | LinkedList | Vector | |
---|---|---|---|
数据结构 | 数组 | 双向列表 | 数组 |
查找 | 有利于 | 不利于 | 有利于 |
增删 | 不利于 | 有利于 | 不利于 |
扩容机制 | 1.5倍扩容 | 无 | 2倍扩容 |
线程安全 | 不安全 | 不安全 | 安全 |
5、HashMap的底层数据结构是什么?
- JDK1.8之前的数据结构是
数组
+链表
,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,数组默认大小是16 - JDK1.8之后数据结构是
数组
+链表
+红黑树
,数组默认大小是16
- 通数组是来存储元素的,链表是来解决冲突的,红黑树是用来提高查询效率
- 数据元素通过映射关系,也就是散列函数,映射到通数组对应索引的位置
- 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
- 如果链表长度>8并且数组>=64,链表转换为红黑树
- 如果红黑树的结点<6,转换为链表
6、怎么解决HashMap的hash冲突?
解决hash冲突的方法:
- 开放定址法(线性探测法):从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲的位置然后把发生冲突的元素放入到这个位置中去。
- 链式寻址法:把hash冲突的key,以单向链表来进行存储
- 再hash法:通过某个hash函数计算的key,存在冲突的时候,再用另一个hash函数去运算,直到没有冲突即可
- 建立公共溢出区:把Hash表分为基本表和溢出表两个部分,凡是存在冲突的元素,一律放到溢出表中去
HashMap在JDK1.8以后使用的是链式寻址法和红黑树来解决Hash冲突的问题,红黑树是为了优化Hash表的链表过长导致时间复杂度增加,增加查询效率。
7、HashMap是线程安全的吗?怎么解决?
HashMap不是线程安全的,会发生死循环和数据丢失的问题(死循环在jdk1.7中存在,在1.8中没有,但是数据丢失在1.7和1.8都存在)。
- 多线程下扩容死循环:jdk1.7的HashMap使用头插法插入元素,在多线程的环境下,扩容的时候可能导致环形链表的出现,jdk1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题
- 多线程put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同的,它会造成前一个key被后一个key覆盖,导致元素的丢失
- put和get并发时,可能导致get为null,线程1执行put时候,因为元素个数超过threshold而导致rehash,线程2此时执行get,有可能导致这个问题
可以使用HashTable、Collections.synchronizedMap、ConcurrentHashMap实现线程安全的Map
- HashTable是直接在操作方法上加synchronized关键字,锁住整个table数组,粒度比较大,效率较低
- Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个synchronizedMap对象,内部定义一个对象锁,方法内通过对象锁实现
- ConcurrentHashMap在1.7中使用分段锁,在jdk1.8中使用CAS+synchronized
8、Hash的扩容策略是什么?
为了减少哈希冲突发生的概率,在当前HashMap的元素个数达到一个临界值的时候,就会触发扩容机制,把所有元素rehash之后再放到扩容后的容器中(1.7时是这样的),扩容因子默认是0.75。
HashMap在jdk1.8中是基于数组+链表和红黑树实现的,但用于存放key值的桶数组的长度是固定的,由初始化参数确定,随着插入的数据增多和负载因子的作用下,需要扩容来存放更多的数据。在jdk1.8中做了优化,可以不用重新计算每一个元素的哈希值,因为HashMap的初始容量是 2的次幂,扩容之后的长度是原来的2倍,新的容量也是2的次幂,所以元素,要么在原来的位置,要么在原位置的再移动2的次幂
9、JDK1.8对HashMap主要做了哪些优化?为什么?
- 数据结构:数组+链表 变成了 数组+链表+红黑树
发生hash冲突,元素会存入链表,链表过长的转换为红黑树
- 链表的插入方式:从头插法改为尾插法
在多线程环境下可能会产生死循环
- 扩容rehash:1.7需要对原数组中的元素重新hash定位到新数组的位置,1.8不需要重新通过哈希函数计算位置,新的位置不变或索引+新增容量大小
提高扩容的效率,更快的扩容
- 扩容时机:1.7先判断是否要扩容,再插入;1.8先进行插入,插入完再判断是否需要扩容
- 散列函数:1.7做了四次移位和四次异或,1.8只做了一次
做4次的话,边际效用也不大,改为一次,提升效率
10、HashMap的put和get流程是什么?
put流程:
- 首先进行hash的扰动,获取一个新的hash值。
- 判断table是否为空或者长度为0,如果是就做扩容操作
- 根据哈希值计算下标,如果对应小标正好没有存放数据,则往下走即可,否则就是覆盖了
- 判断table[i]是否是树节点,是就往树中插入,否则就插入到链表中
- 如果链表中插入节点的时候,如果达到了树化的阈值,就要把链表转换为红黑树
- 最后所有元素处理完成后,判断是否超过阈值,超过则扩容
get流程:
- 使用扰动函数(
降低哈希碰撞的概率
),获取新的哈希值 - 计算数组下标,获取节点
- 当前节点和key匹配,直接返回
- 否则,当前节点是否为树节点,查找红黑树
- 否则,遍历链表查找
11、ConcurrentHashMap底层具体实现?1.7和1.8有什么不同?
1.8之前:
这里的ConcurrentHashMap
是由Segment
和HashEntry
数组结构组成。
Segment
继承了ReentrantLock
,所以Segment
是一种可重入锁,它里面的每一段配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
一个ConcurrentHashMap
中包含了一个Segment
数组,Segment的个数一旦初始化就不能改变。Segment
数组的大小默认是16,最多支持16个线程并发。
1.8之后:
这里ConcurrentHashMap是由Node+CAS+synchronized组成。
数据结构和HashMap1.8结构类似。链表长度超过了一定阈值的时候,就会转换为红黑树。
1.8版本中,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,不会影响其它Node的读写,效率提升。
1.7和1.8有什么不同呢?
-
线程安全实现方式:jdk1.7采用
Segment
分段锁来保证安全,Segment
是继承自ReentrantLock
,jdk1.8放弃了Segment
分段锁的设计,采用Node + CAS + synchronized
保证线程安全,锁的粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。 -
Hash碰撞解决方法:jdk1.7采用拉链法,jdk1.8采用拉链法结合红黑树
-
并发度:jdk1.7最大并发度是Segment的个数,默认是16,jdk1.8最大并发度是Node数组的大小,并发度更大。
12、Set 和 Map的区别,List 和 Set 的区别?
Set和Map
List | Set | Map | |
---|---|---|---|
继承接口 | Collection | Collection | |
常见实现接口 | ArrayList、LinkedList、Vector | HashSet、LinkedHashSet、TreeSet | HashMap、HashTable |
常见方法 | add()、remove()、size()… | add()、remove()、size()… | put()、get()、remove()、clear()、values()、size()… |
是否可重复 | 可 | 否 | 否 |
线程安全 | Vector安全 | 否 | HashTable安全 |