在Java中,STL(Standard Template Library)是一组容器类的集合,用于存储和操作数据。这些容器类提供了一种方便的方式来处理和管理数据,而不必关心底层的实现细节。
List、Set、Queue、Map的区别
- List链表:存储的元素是有序的、可重复的
- Set集合:存储的元素是不可重复的
- Queue队列:按照特定的排队规则来确定先后顺序,有序、可重复
- Map队列:使用键值对存储,每个键最多映射到一个值
ArrayList和LinkedList的区别
ArrayList | LinkedList | |
线程安全? | 不同步,不保证安全 | 不同步,不保证安全 |
底层数据结构 | 数组 | 双向链表 |
插入和删除是否受位置元素影响 | 影响 | 不影响 |
是否支持快速随机访问 | 支持 | 不支持 |
内存空间占用 | 空间浪费体现在链表尾部会预留一定的容量空间 | 每个元素都要比arrayList消耗更多的空间【存储后继、直接前驱及数据】 |
HashMap的底层原理
主要用来存放键值对,基于哈希表的map接口实现,是非线程安全的。
HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个
JDK1.8之前
由数组+链表组成,数组是HashMap的主体,链表主要是为了解决哈希冲突而存在的(拉链法)
拉链法:
就是将数组和链表结合,也就是创建一个链表数组,数组中的每个格就是一个链表。如遇到哈希冲突,就把冲突值加到链表中即可
JDK1.8
由数组+链表+红黑树,解决哈希冲突的方式有改变
- 当链表长度大于8(链表转为红黑树之前会进行判断,如果数组长度小于64,会优先对数组进行扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
这里为什么用红黑树,而不是其他树?
红黑树查询时间复杂度O(logn),适用于大量的插入和删除。
当节点数目比较少时,B树/B+树会挤在一起成链表,而链表查询效率比较低。
HashMap的长度为什么是2的幂次
HashMap的初始化大小是16,每次扩容,容量会变为之前的2倍。因此HashMap 总是使用 2 的幂作为哈希表的大小。
HashMap为什么是线程不安全的
JDK1.8之前的版本
多线程环境下扩容可能存在死循环和数据丢失的问题。【当一个桶位中有多个元素需要扩容时,多个线程同时对链表进行操作,头插法可能导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环而无法结束】
JDK1.8 版本
HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
在 HashMap
中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap
的 put
操作会导致线程不安全,具体来说会有数据覆盖的风险
举个栗子
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// ...
// 判断是否出现 hash 碰撞
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素(处理hash冲突)
else {
// ...
}
- 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
- 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
- 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。
HashMap和Hashtable的区别
是否线程安全 | null | 初始容量和扩容大小 | |
HashMap | 否 | 支持一个null key,多个null value | 初始容量16,扩容大小2n |
Hashtable | 是【内部方法由synchronized 修饰 】 | 不允许 | 默认容量11,扩容2n+1 |
ConcurrentHashMap实现方法不同jdk1.7和jdk1.8
- 线程安全实现方式:JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
- Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
- 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。