1.1JAVA基础面试
1.2JAVA 集合面试
1.2.1 HashMap 和 Hashtable:区别、联系与实战解析
共同点:
底层数据结构(核心):二者底层都基于「数组 + 链表」(JDK 1.8 后 HashMap 新增红黑树优化)实现哈希表。通过 key.hashCode() 计算哈希值,确定元素在数组中的位置;哈希冲突时,用链表(或红黑树)存储冲突元素。
核心功能:都实现了 Map 接口,支持「增删改查」键值对操作(put/get/remove 等)
键的要求:键都不能重复(重复 put 会覆盖原值);键的 hashCode() 和 equals() 方法会影响哈希表的正确性(需遵循重写规则)。
核心区别
| 维度 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 非线程安全(性能高) | 线程安全(性能低) |
| 底层实现 | JDK 1.8 引入红黑树(链表长度≥8 时转换) | 始终是数组 + 链表(无红黑树优化) |
| null 支持 | 允许 key/value 为 null(仅 1 个 null key) | 不允许 key/value 为 null(抛 NullPointerException) |
| 继承体系 | 继承 AbstractMap,实现 Map 接口 |
继承 Dictionary,实现 Map 接口 |
| 初始容量 / 扩容 | 初始容量 16,扩容为 2 倍(16→32→64) | 初始容量 11,扩容为 2 倍 + 1(11→23→47) |
| 哈希算法 | 优化过的哈希算法(减少冲突) | 直接使用 key.hashCode()(冲突率较高) |
| 遍历方式 | 支持快速失败(fail-fast)的迭代器 | 支持快速失败的枚举(Enumeration)+ 迭代器 |
| 默认加载因子 | 0.75(和 Hashtable 一致) | 0.75 |
| 使用场景 | 单线程环境(绝大部分业务场景) | 多线程环境(已被 ConcurrentHashMap 替代) |
为什么 Hashtable 被淘汰?
Hashtable 的线程安全是通过「给整个方法加 synchronized」实现的,相当于 "整个哈希表一把锁":
- 线程 A 操作索引 0 的元素,线程 B 操作索引 100 的元素,也会被阻塞;
- ConcurrentHashMap 采用「分段锁(JDK 1.7)/ CAS + 局部锁(JDK 1.8)」,只锁冲突的桶,并发性能提升 10 倍以上。
1.2.2 为什么ArrayList不是线程安全的,具体来说是哪里不安全?
在高并发添加数据下,ArrayList会暴露三个问题;
- 部分值为null(我们并没有add null进去)
- 索引越界异常
- size与我们add的数量不符
java
public boolean add(E e) {
ensureCapacityInternal(size + 1);// Increments modCount!!
elementData[size++]=e;
return true;
}
大体可以分为三步:
- 判断数组需不需要扩容,如果需要的话,调用grow方法进行扩容;
- 将数组的size位置设置值(因为数组的下标是从o开始的);
- 将当前集合的大小加1
下面我们来分析三种情况都是如何产生的:
- 部分值为null:当线程1走到了扩容那里发现当前size是9,而数组容量是10,所以不用扩容,这时候cpu让出执行权,线程2也进来了,发现size是9,而数组容量是10,所以不用扩容,这时候线程1继续执行,将数组下标索引为9的位置set值了,还没有来得及执行size++,这时候线程2也来执行了,又把数组下标索引为9的位置set了一遍,这时候两个先后进行size++,导致下标索引l10的地方就为null了。
- 索引越界异常:线程1走到扩容那里发现当前size是9,数组容量是10不用扩容,cpu让出执行权,线程2也发现不用扩容,这时候数组的容量就是10,而线程1set完之后size++,这时候线程2再进来size就是10,数组的大小只有10,而你要设置下标索引为10的就会越界(数组的下标索引从0开始);
- size与我们add的数量不符:这个基本上每次都会发生,这个理解起来也很简单,因为size++本身就不是原子操作,可以分为三步:获取size的值,将size的值加1,将新的size值覆盖掉原来的,线程1和线程2拿到一样的size值加完了同时覆盖,就会导致一次没有加上,所以肯定不会与我们add的数量保持一致的;
1.2.3 什么是CAS
CAS 是「比较并交换」(Compare And Swap)的缩写,是无锁并发编程 的核心技术(也是 ConcurrentHashMap、AtomicInteger 等并发工具的底层实现)。它解决了传统加锁(synchronized)导致的性能问题,用硬件级别的原子操作保证并发安全。
核心定义:CAS 是一种原子指令(由 CPU 硬件层面保证原子性),它的逻辑可以概括为:我认为变量的当前值是 A,只有当实际值确实是 A 时,才把它改成 B;如果实际值不是 A(说明被其他线程改了),就什么都不做,返回 "失败"。
CAS vs synchronized(核心对比)
| 维度 | CAS | synchronized |
|---|---|---|
| 锁类型 | 无锁(自旋) | 独占锁(阻塞) |
| 原子性保证 | CPU 硬件指令 | JVM 层面的锁机制 |
| 性能 | 高(无上下文切换) | 低(高并发下阻塞 / 唤醒开销) |
| 适用场景 | 单个变量的高频读写 | 多变量 / 复杂逻辑的并发控制 |
| 问题 | ABA、自旋开销 | 死锁、线程阻塞 |