文章目录
- 1.讲一下集合是什么/怎么理解集合的
- 2.set、list、map的区别?
- 3.集合的底层数据结构
- 4.如何选取集合结构?
- 5.说一下阻塞队列的实现原理
- 6.List
- 7.Map
- HashSet如何检查重复?
- HashMap的内部数据结构
- 为什么HashMap中链表转红黑树的阀值是8?
- HashMap中put方法过程
- 为什么HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂?
- Hashmap怎么扩容?
- 为什么Hashmap扩容后是原先的两倍?
- Hashmap存在什么安全问题,怎么解决
- HashMap多线程下操作导致死循环问题
- ConcurrentHashMap 的实现原理是什么?
- ConcurrentHashMap和Hashtable
- JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?
- JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?
- ConcurrentHashMap 的 get 方法是否要加锁,为什么?
1.讲一下集合是什么/怎么理解集合的
集合就是一个放数据的容器,准确的说是放数据对象引用的容器。
集合类存放的都是对象的引用,而不是对象的本身。
集合类型主要有 3 种: set(集)、 list(列表)和 map(映射) 。
一类是实现Collection接口;另一类是实现Map接口。
2.set、list、map的区别?
- List
(1)可以允许重复的对象;
(2)可以插入多个null元素;
(3)是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序; - Set
(1)不允许重复的对象;
(2)无序容器,你无法保证每个元素的存储顺序。
(3)只允许一个null元素;
(4)HashSet是基于HashMap实现的 - Map
(1)Map不是collection的子接口或者实现类,Map是一个接口;
(2)HashMap底层是数据+链表的组成,是无序的,通过hashCode()方法计算索引值后再存储或查找元素;链表长度大于8时转换成数组+红黑树结构;
(3)Map的每个Entry都持有俩个对象,一个键一个值,可能会持有相同的值对象但键对象必须是唯一的(key相同会覆盖之前相同的key值);
(4)Map里你可以拥有任意个null的value值,但只能有一个null的key键;
3.集合的底层数据结构
- List
- ArrayList Object[]数组
- Vector Object[]数组
- LinkedList 双向链表(jdk1.6之前是循环链表,1.7取消了循环)
- Set
- HashSet(无序、唯一) 底层采用HashMap来保存元素
- TreeSet(有序、唯一) 底层采用红黑树(自平衡的二叉排序树)实现
- Queue
- ArrayQueue Object[]数组 + 双指针
- PriorityQueue Object[]数组来实现二叉堆
- Map
- HashMap: jdk1.8之前由数组+链表组成,数组是HashMap的主体,而链表主要是为了解决哈希冲突。jdk1.8之后引入了红黑树。当链表长度大于等于8且数组长度大于等于64,链表就会转化为红黑树。如果只满足一个条件,那么会优先选择数组扩容。
- TreeMap: 红黑树(自平衡的二叉排序树),底层是基于TreeSet实现的。
- HashTable: 数组+链表组成,数组是HashTable的主题,链表则是主要是为了解决哈希冲突而存在的。
4.如何选取集合结构?
- 如果我们需要根据键值来获取元素值,就可以选用Map接口下的集合。
- 需要排序就选用TreeMap,不需要排序就选用HashMap,需要保证线程安全就选用ConcurrentHashMap;
- 如果我们只需要存放元素值,就选择实现Collection接口的集合。
- 需要保证元素唯一时选择Set接口下的集合,比如TreeSet或者是HashSet,不需要就选用List接口下的集合,比如ArrayList或者LinkedList。
5.说一下阻塞队列的实现原理
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:
- 在队列为空时,获取元素的线程会等待队列变为非空。
- 当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
6.List
ArrayList扩容
在 Java 中,ArrayList是一个动态数组,它可以根据需要自动增长。当 ArrayList 中的元素数量超过其初始容量时,它会重新分配一个更大的内部数组,然后将现有元素复制到新数组中。这个过程称为扩容。
list和Arrays排序方法
Collection.sort是对list进行排序,Arrays.sort是对数组进行排序。
数组和List相互转换
List.toArray();
Arrays.asList(new Integer[]{1,2,3,4,5})
Arraylist和Linkedlist相互转换
//方法一
ArrayList arrayList = new ArrayList();
LinkedList linkedList = new LinkedList(arrayList);
LinkedList linkedList = new LinkedList();
ArrayList<String> arrayList = new ArrayList(linkedList);
//方法二
ArrayList<String> arrayList = Arrays.asList(str);
LinkedList<String> list = arrayList.stream().collect(Collectors.toCollection(LinkedList::new));
Arraylist和Linkedlist相互转换
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
- 随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,
相对于ArrayList,LinkedList的插入,添加,删除操作速度更快。因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。 - LinkedList比ArrayList更占内存,因为LinkedList每一个节点存储了两个指向上下的引用
Arraylist和数组的区别
- ArrayList是动态扩容的,数组无法进行动态扩容。
- 迭代方式不同,数组只能通过循环,通过元素的索引下标来达到一一遍历的目的,而 ArrayList 不仅可以使用循环来遍历,还可以通过Iterator来进行遍历
- 数组中的元素可以是基本数据类型或者是引用类型,而 ArrayList 只支持引用类型,ArrayList 中对于基础数据类型的储存先要将其自动装箱成对应的包装类,取出的时候也是相同的,自动拆箱成基础数据类型。
- 数组在构建的时候需要声明容纳元素的类型,而 ArrayList 则不需要声明容纳元素的类型
7.Map
Hashset和Hashmap区别
- HashSet 集合不允许存储相同的元素, 它底层实际上使用 HashMap 来存储元素的, 数据添加到key, 所有value元素默认为static final修饰的Object类对象。
- HashMap实现了Map接口,HashSet实现了Set接口
- HashMap储存键值对,HashSet存储对象
- 使用put()方法将元素放入map中,使用add()方法将元素放入set中
HashSet如何检查重复?
当把对象加入到HashSet中时,HashSet会先计算对象的hashCode值来判断对象加入的下标位置,同时也会与其他的对象的hashCode进行比较。
- 如果没有相同的,就直接插入数据;
- 如果有相同的,就进一步使用equals来进行比较对象是否相同,如果相同,就不会加入成功。
HashMap的内部数据结构
Hashmap7是数组+链表,Hashmap8是数组+链表+红黑树(链表大于8时)
为什么HashMap中链表转红黑树的阀值是8?
因为泊松分布的原则(超过8的话的概率非常非常小),且必须是2的幂次方的要求,所取得的最合适的值。
HashMap中put方法过程
先对key求hash值,如果没有碰撞,直接放入哈希桶中,如果碰撞了(哈希冲突),以链表的方式链接到后面。
如果链表长度超过阀值8,就把链表转成红黑树,如果节点已经存在就替换旧值
为什么HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂?
- 为了数据的均匀分布,减少哈希碰撞。
因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次) - 手动容量时输入数据若不是2的幂,HashMap通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字。
Hashmap怎么扩容?
当向容器添加元素的时候会判断 当前元素个数 > 数组的长度乘以加载因子(默认0.75)的值的时候,就要自动扩容为原先的两倍。
为什么Hashmap扩容后是原先的两倍?
因为HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,会减少哈希碰撞,避免形成链表的结构。
Hashmap存在什么安全问题,怎么解决
- 因为HashMap是线程不安全,所以在多线程的情况下会导致数据的覆盖。
同时1.8之前的话扩容可能会发生死循环。 - 可以使用ConcurrentHashMap
HashMap多线程下操作导致死循环问题
主要原因是并发下的Rehash会造成元素之间形成一个循环链表。多线程下HashMap会存在数据丢失的问题,并发环境下推荐使用ConcurrentHashMap
ConcurrentHashMap 的实现原理是什么?
-
JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构(外层)和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成(HashEntry内部相当于HashMap里面的Entry对象)。
Segment继承了ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。 -
JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+(链表长度大于8时)红黑树结构;
在锁的实现上,抛弃了原有的 Segment 分段锁,采用 Synchronized来控制对桶的操作,同时结合了 CAS(Compare And Swap)和 Node 数据结构来实现并发操作。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。而且是只有put和remove才会上锁,get是不需要上锁 -
1.8实现原理:会先进行cas操作,如果失败,会判断是不是有其他线程在扩容,如果还不是的话会使用synchronized 锁插入元素
ConcurrentHashMap和Hashtable
- ConcurrentHashMap 的效率要高于 Hashtable。
因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。
而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。
JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?
因为在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态。
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。只有链表的头节点(红黑树的根节点)需要同步
JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?
- 数据结构:JDK1.8取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:
JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中Segment 继承自 ReentrantLock 。
JDK1.8 采用CAS+synchronized保证线程安全。 - 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
ConcurrentHashMap 的 get 方法是否要加锁,为什么?
get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用volatile修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。