1、ArrayList和LinkedList的区别
ArrayList和LinkedList都是基于List接口实现的,但底层结构不同
ArrayList底层基于动态数组,支持随机访问因为根据下标进行访问,但是插入和删除的话就会因为元素的移动造成效率比较低。他不是线性安全的,只适合于在单线程下频繁的读取数据
LinkedList底层是双向链表,每个节点包含者前驱和后继,它在任意位置插入和删除元素效率都会很高,前提情况下是定位到位置,但是在随机访问的时候就需要遍历整个链表导致访问速度慢,它也不是线性安全的。
读多写少->
ArrayList频繁在头部或者尾部增删元素->
LinkedList如何让他们变的线程安全呢
在方法内使用,局部变量则是线程安全的
用工具类包装,生成一个线程安全的代理版本
List<String> list = Collections.synchronizedList(new ArrayList<>());
2、HashMap得底层原理是什么,JDK8之后有什么优化?
HashMap底层就是由一个Node<K,v>[]数组构成,每个数组位置称为一个桶,每个桶存储一个链表和红黑树的头节点。
在JDK8之前,当发生哈希冲突的时候,使用链表解决,最坏的情况就是查询时间是O(n)。当数据量大、链表过长时,性能急剧下降。
然后JDK8引入了优化:当某个桶的链表长度达到8,并且数组长度>=64时,该链表会转换成红黑树,使查找、插入、删除的时间复杂度从O(N)降低到O(logN)
如果链表长度达到8但数组长度<64,则会优先进行扩容,而不是转树,以期望通过扩容分散哈希冲突
这个阈值8是基于泊松分布计算出来的,正常情况下极少达到,转树主要是为了防御恶意攻击。
3、ConcurrentHashMap是怎么保证线程安全的?和HashMap有什么不同?
在JDK8中,concurrentHashMap不再使用Segment分段锁,而是采用更细粒度的同步策略
底层结构依然是数组+链表/红黑树,和HashMap类似
线程安全的实现方法是:
-
使用
CAS操作来保证初始化和扩容时的线程安全 -
在put元素的时候,如果该桶有多个元素,就是用synchronized锁住链表的头节点或红黑树的根节点
-
锁的粒度非常小,只有多个线程用时修改同一个桶才会有竞争,大大提升了并发性能
-
而读操作完全无锁,依赖volatile保证可见性,所有读性能几乎和非线性安全容器一样高
想象一个图书馆:
HashMap:谁都能改书架,没人管 → 乱
Hashtable:整个图书馆只有一把大锁 → 谁进都得等
ConcurrentHashMap(JDK 8):每个书架自己有小锁 → 你改 1 号书架,我不影响改 2 号
4、HashMap的key为什么建议用String或Integer?如果我们自定义key的话注意什么?
String和Integer被广泛用作HashMap的key,是因为它们具备做key的理想特性:
-
不可变性:一旦创建,
hashCode不会变,适合做key -
正确重写了
hashCode()和equals():能保证逻辑一致 -
线程安全:无状态,多线程访问安全
当我们使用自定义对象作为key时,必须:
-
重写
equals方法:定义对象相等的业务逻辑 -
重写
hashCode方法:保证相等的对象有相同的哈希值 -
最好将key设为不可变对象:避免
hashCode变化导致无法定位桶
否则可能导致对象存进去却取不出来,因为HashMap依赖hashCode定位桶,再用equals判断是否真相同。
最后一句金句就是
hashCode()决定去哪桶,equals决定是不是你
5、HashMap的key可以为null吗?get(null)怎么工作的?多(null)key会怎样?
HashMap对null键做了处理
-
在put和get的时候,会判断key是否为null
-
如果时null,就会被调用
putForNullKey()或者getForNullKey()这类专用方法 -
null键对应的Entry被存储在数组的第0个位置
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first;
if (key == null)
return getNullKeyNode(); // 专门处理 null 键
// ...
}
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); // 专门的方法处理 null
// ...
}
“
HashMap扩容时,并不会创建新的HashMap实例,而是创建一个长度为原来两倍的新数组。”“然后遍历旧数组中的每个链表,将节点重新分配到新数组中。”
“
JDK 8的优化在于:由于数组长度是 2 的幂,扩容后只需看hash值的一个高位 bit:”
“如果该位为 0,节点留在原索引位置”
“如果该位为 1,节点移到 ‘原索引 + 旧容量’ 的位置”
“这样就避免了对每个 key 重新计算
hash,提升了扩容效率。”
6、TreeMap和HashMap的区别
-
“
HashMap:基于哈希表 + 链表/红黑树,不保证顺序,查询性能高(O(1)),适合大多数场景。” -
“
LinkedHashMap:在HashMap基础上加了双向链表,维护插入顺序或访问顺序,适合需要顺序遍历或实现 LRU 缓存的场景。” -
“
TreeMap:基于红黑树,键有序,支持自然排序或自定义Comparator,查询、插入、删除时间复杂度为 O(log n),适合需要排序或范围查询的场景(如排行榜、区间查找)。”
“所以:”
-
“要快 →
HashMap” -
“要顺序 →
LinkedHashMap” -
“要排序 →
TreeMap”
// 自然排序
TreeMap<String, Integer> map1 = new TreeMap<>();
// 自定义排序:按字符串长度排
TreeMap<String, Integer> map2 = new TreeMap<>((a, b) -> a.length() - b.length());
7、fail-fast与fail-safe
fail-fast
1、fail-fast是什么快速失败
抽象点就是我正在读一本书(遍历集合),这个时候突然有人走了进来对我正在遍历的集合进行删除或者修改,我就会说书变了!我不读了。
它是对所有的非线程安全集合都在检查会抛出ConcurrentModificationException
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
// 边遍历边删除 → 危险!
for (String s : list) {
if (s.equals("a")) {
list.remove(s); // ❌ 抛 ConcurrentModificationException
}
}
源码核心
// ArrayList 内部
int modCount = 0; // 修改次数
// 迭代器创建时
int expectedModCount = modCount; // 记下“我开始时的版本”
// 遍历时检查
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
就像你读书时,书页右上角有个编号:
-
你开始读时是
v1 -
有人改了,变成
v2 -
你一翻页,发现编号变了 → “书被改了!” → 抛异常
这里可以使用迭代器删除就可以安全不用报错
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("a")) {
it.remove(); // ✅ 允许,迭代器会同步 modCount
}
}
fail-safe
它就是不管集合到底修没修改,都不影响我读
哪些集合
-
ConcurrentHashMap -
CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
// 遍历
for (String s : list) {
list.add("c"); // ✅ 不会抛异常!
System.out.println(s);
}
为什么安全?—— 看 CopyOnWrite
CopyOnWriteArrayList 的原理:
-
读操作:直接读内部数组,不加锁
-
写操作:复制一份新数组,在新数组上修改,然后替换引用
private transient volatile Object[] array;
public boolean add(E e) {
Object[] oldArray = array;
Object[] newArray = Arrays.copyOf(oldArray, oldArray.length + 1);
newArray[newArray.length - 1] = e;
array = newArray; // 原来的数组还在,迭代器继续读它
}
-
他不是改你手里的书,
-
而是复印一本新书,在新书上改
-
你继续读旧书,不受影响
总结
fail-fast 和 fail-safe 是集合迭代器处理并发修改的两种策略。”
“fail-fast:大多数非线程安全集合(如 ArrayList、HashMap)采用的策略。迭代器会记录 modCount,一旦发现集合被外部修改,立即抛出 ConcurrentModificationException。这是一种‘快速报错’机制,提醒开发者注意并发问题。”
“fail-safe:ConcurrentHashMap、CopyOnWriteArrayList 等并发集合采用的策略。迭代器基于集合的快照或当前状态进行遍历,原集合的修改不会影响迭代过程,不会抛异常。但读到的数据可能不是最新的(弱一致性)。”
“所以:”
-
“
fail-fast:发现问题立刻报,适合单线程或加锁场景” -
“
fail-safe:你改你的,我读我的,适合并发读场景”
8、ArrayList底层实现原理
-
它是基于动态的数组实现的
-
当第一次添加的时候,这个数组长度初始化是10
-
ArrayList在扩容的时候是原来的1.5倍,然后将数组拷贝到扩容的数组中 -
ArrayList在添加数据的时候-
确保数组已使用的长度+1能够存下下一个数据
-
计算数组的容量,如果数组已使用长度+1后大于当前的数组长度,则调用grow方法扩容
-
确保新增的数据有地方存储,则将新元素添加到位于size的位置上
-
返回添加成功布尔值
-
9、HashMap和ConcurrentHashMap在并发场景下的主要区别有五点
-
线程安全:
HashMap不安全,多线程下可能死循环或数据丢失;ConcurrentHashMap安全,采用 CAS 和synchronized锁桶头节点。 -
锁粒度:
ConcurrentHashMap锁的是单个桶的头节点,不同桶可并发操作,性能高。 -
扩容机制:
ConcurrentHashMap支持多线程协作扩容,多个线程可同时迁移数据,提升扩容效率。 -
null 值限制:
ConcurrentHashMap不允许key或value为null,避免get返回null时的语义歧义。 -
性能权衡:
HashMap单线程性能更好,ConcurrentHashMap在并发场景下更稳定可靠。
“所以,单线程用 HashMap,多线程必用 ConcurrentHashMap。”

475

被折叠的 条评论
为什么被折叠?



