集合
集合
概念
数组与集合区别,用过哪些?
数组和集合的核心区别
- 类型与泛型
- 数组:声明时必须明确元素类型(如
int[]
、String[]
),只能存储同类型元素(基本类型或引用类型)。 - 集合:默认以
Object
存储元素,支持泛型(如List<String>
)增强类型安全,未用泛型时可混合类型。
- 数组:声明时必须明确元素类型(如
- 容量与动态性
- 数组:长度固定,无法扩容,需手动处理增删(如复制数组)。
- 集合:动态扩容(如
ArrayList
),提供add()
、remove()
等灵活操作。
- 性能与内存
- 数组:内存连续分配,索引访问 O(1),适合高频访问。
- 集合:底层结构多样(链表、哈希表等),部分操作效率低(如
LinkedList
随机访问为 O(n))。
- 功能扩展
- 数组:仅支持基础操作(赋值、遍历)。
- 集合:提供排序(
Collections.sort()
)、过滤(Stream.filter()
)、线程安全工具(如Collections.synchronizedList()
)等丰富 API。
- 适用场景
- 数组:数据量固定、性能敏感场景(数学计算、固定配置项)。
- 集合:数据动态变化、需复杂操作的场景(订单管理、用户日志)。
- 类型与泛型
常见的集合类型及使用场景
- List(有序、可重复)
ArrayList
:基于动态数组,快速随机访问,适合读多写少(商品列表展示)。LinkedList
:基于双向链表,插入/删除高效,适合频繁增删(消息队列)。
- Set(无序、唯一)
HashSet
:基于哈希表,查找 O(1),适合去重或快速查找(用户黑名单)。TreeSet
:基于红黑树,自动排序,适合有序唯一需求(排行榜)。
- Map(键值对)
HashMap
:非线程安全,允许null
键值,适合高频读写(缓存数据)。ConcurrentHashMap
:线程安全,适合高并发(分布式计数器)。
- 其他工具类
Collections
:提供集合工具方法(排序、同步化包装)。Arrays
:提供数组工具方法(asList()
、sort()
)。
- List(有序、可重复)
实际使用经验举例
- 动态数据管理:用
ArrayList<FormData>
存储用户表单数据,通过泛型保证类型一致。 - 高频查询优化:用
HashMap
缓存数据库结果(如用户ID到对象的映射),减少重复查询。 - 并发场景处理:多线程下用
ConcurrentHashMap
替代Hashtable
,兼顾性能与线程安全。
- 动态数据管理:用
总结
- 选择依据:优先考虑数据量是否固定、是否需要动态操作、线程安全及性能需求。
- 推荐实践:默认使用集合(如
ArrayList
、HashMap
),仅在数据量固定或追求极致性能时用数组。
说说 Java 中的集合?
核心接口与实现类
- Collection接口
- List(有序、可重复)
ArrayList
:基于动态数组,快速随机访问(O(1)),适合读多写少(如商品列表)。LinkedList
:基于双向链表,插入/删除高效(O(1)),适合频繁增删(如消息队列)。Vector
:线程安全的动态数组,性能较低,逐渐被ArrayList
替代。
- Set(无序、唯一)
HashSet
:基于哈希表,查找 O(1),适合去重或快速查找(如用户黑名单)。TreeSet
:基于红黑树,元素自动排序,适合有序唯一需求(如排行榜)。
- Queue(队列)
LinkedList
:支持队列(FIFO)和栈(LIFO)。PriorityQueue
:基于优先级堆,元素按优先级排序。
- List(有序、可重复)
- Map接口(键值对)
HashMap
:非线程安全,允许null
键值,适用于高频读写(如缓存)。ConcurrentHashMap
:线程安全,分段锁机制,适合高并发场景。TreeMap
:基于红黑树,键自动排序,适合有序键场景(如按日期排序日志)。
- Collection接口
核心特性对比
特性 数组 集合 类型支持 支持基本类型和对象 仅支持对象(需通过包装类处理基本类型) 容量动态性 固定长度 动态扩容(如 ArrayList
自动扩展)性能 内存连续,随机访问 O(1) 数据结构多样(如链表 O(n) 访问) 功能扩展 仅基础操作 提供排序、过滤、线程安全等丰富 API 线程安全与工具类
- 线程安全实现
- 同步容器:
Vector
、Hashtable
(通过synchronized
实现,性能低)。 - 并发容器:
ConcurrentHashMap
、CopyOnWriteArrayList
(分段锁或写时复制技术)。
- 同步容器:
- 工具类
Collections
:提供排序(sort()
)、查找(binarySearch()
)、同步化包装(synchronizedList()
)。Arrays
:支持数组转换(asList()
)、排序(sort()
)。
- 线程安全实现
设计原则与最佳实践
- 泛型与类型安全
- 集合通过泛型(如
List<String>
)限制元素类型,避免运行时错误。
- 集合通过泛型(如
- 选择依据
- 数据量固定:优先用数组(如数学计算)。
- 动态操作:优先用集合(如
ArrayList
、HashMap
)。 - 排序需求:使用
TreeSet
或TreeMap
。 - 并发场景:使用
ConcurrentHashMap
或CopyOnWriteArrayList
。
- 性能优化
- 避免集合频繁扩容(如预初始化
ArrayList
容量)。 - 使用迭代器(
Iterator
)遍历,避免并发修改异常。
- 避免集合频繁扩容(如预初始化
- 泛型与类型安全
扩展与演进
- 历史演进:Java 1.0 的
Vector
、Hashtable
逐渐被 Java 2 集合框架替代。 - 函数式编程:Java 8 引入流(
Stream
)和 Lambda,支持链式操作(过滤、映射)。
- 历史演进:Java 1.0 的
Java 中的线程安全的集合是什么?
传统同步集合(低并发场景)
Vector
:基于动态数组,所有方法用synchronized
修饰,线程安全但性能低,适合简单场景。Hashtable
:线程安全哈希表,不允许null
键值,方法同步导致性能差,已被ConcurrentHashMap
取代。Stack
:继承自Vector
,实现后进先出(LIFO)的栈结构,同步机制与Vector
一致。- 同步包装类:通过
Collections.synchronizedList()
、Collections.synchronizedMap()
包装普通集合为线程安全版本,但需手动同步复合操作(如迭代)。
并发集合(
java.util.concurrent
包,高并发场景)ConcurrentHashMap
:线程安全哈希表,JDK 1.8 前分段锁,之后改为CAS + synchronized
,支持高并发读写。CopyOnWriteArrayList
/CopyOnWriteArraySet
:写时复制(Copy-On-Write)机制,读多写少场景适用(如配置缓存)。ConcurrentLinkedQueue
/ConcurrentLinkedDeque
:无锁(CAS 实现)线程安全队列,适合高并发生产者-消费者模型。ConcurrentSkipListMap
/ConcurrentSkipListSet
:基于跳表(Skip List),支持有序键值对的线程安全操作(如排行榜)。
阻塞队列(
BlockingQueue
接口实现类)ArrayBlockingQueue
:基于数组的有界队列,通过ReentrantLock
实现线程安全。LinkedBlockingQueue
:基于链表的可选有界队列(默认无界),适合任务队列场景。PriorityBlockingQueue
:无界优先级队列,元素按自然顺序或自定义Comparator
排序。SynchronousQueue
:不存储元素,直接传递任务,适用于线程间直接交换数据。
线程安全集合的选择依据
- 高并发读写:优先用
ConcurrentHashMap
(Map)或ConcurrentLinkedQueue
(队列)。 - 读多写少:选择
CopyOnWriteArrayList
或CopyOnWriteArraySet
。 - 有序性需求:使用
ConcurrentSkipListMap
或ConcurrentSkipListSet
。 - 生产者-消费者模型:采用
BlockingQueue
实现类(如LinkedBlockingQueue
)。 - 兼容旧代码:临时用
Vector
或Hashtable
,但不推荐高并发场景。
- 高并发读写:优先用
注意事项
- 复合操作安全性:即使使用线程安全集合,组合多个方法(如“检查-修改”)仍需额外同步。
- 迭代器行为:
ConcurrentHashMap
迭代器是弱一致性的。CopyOnWriteArrayList
迭代器基于快照,不会抛出ConcurrentModificationException
。
- 性能权衡:同步集合(如
Vector
)适合低并发,并发集合(如ConcurrentHashMap
)高并发更优。
Collections 和 Collection 的区别
定义与性质
- Collection:Java 集合框架的根接口,定义集合的通用方法(如
add()
、remove()
),所有集合类(List
、Set
、Queue
)均直接或间接实现此接口。 - Collections:工具类,提供静态方法操作集合(如排序、线程安全包装),不存储数据,仅增强集合功能。
- Collection:Java 集合框架的根接口,定义集合的通用方法(如
用途与功能
- Collection
- 规范集合行为,如
List
(有序可重复)、Set
(无序唯一)、Queue
(队列)。 - 实现类包括
ArrayList
、HashSet
、PriorityQueue
等。
- 规范集合行为,如
- Collections
- 提供功能扩展:
sort()
(排序)、synchronizedList()
(线程安全包装)、binarySearch()
(二分查找)。
- 提供功能扩展:
- Collection
方法类型与调用方式
- Collection
- 实例方法:通过集合对象调用(如
list.add("Java")
)。
- 实例方法:通过集合对象调用(如
- Collections
- 静态方法:直接通过类名调用(如
Collections.sort(list)
)。
- 静态方法:直接通过类名调用(如
- Collection
子类与扩展性
- Collection
- 通过子接口(如
List
、Set
)和实现类扩展数据结构。
- 通过子接口(如
- Collections
- 无子类,所有方法独立存在,适用于任何
Collection
实现类。
- 无子类,所有方法独立存在,适用于任何
- Collection
线程安全性
- Collection
- 默认非线程安全(如
ArrayList
、HashMap
),需手动同步或使用并发集合(如ConcurrentHashMap
)。
- 默认非线程安全(如
- Collections
- 提供同步包装方法(如
Collections.synchronizedList(list)
),但迭代时仍需手动加锁。
- 提供同步包装方法(如
- Collection
对比表
对比项 Collection Collections 类型 接口 工具类 方法类型 实例方法 静态方法 线程安全 默认不安全 提供同步包装方法 核心作用 定义数据结构规范 增强集合功能
集合遍历的方法有哪些?
原始人模式
- 普通 for 循环:靠索引点名,
List
专属,Set
直接装死。 - 增强 for 循环:无脑自动导航,但手贱删元素会原地爆炸(抛异常)。
- 普通 for 循环:靠索引点名,
智能导航
- 迭代器:自带安检员,边逛边删不翻车,但只能一路向前(单向)。
- 列表迭代器:双向飙车特技,
List
专属,还能改户口(修改元素)。
懒人套餐
- forEach:一键清空购物车(遍历),但别想中途反悔(不支持
break
)。 - Stream API:流水线魔法,边遍历边加工(过滤/统计/变形),附赠多线程加速包。
- forEach:一键清空购物车(遍历),但别想中途反悔(不支持
Map 专场
- entrySet:抄家式遍历(键值对一锅端),效率之王。
- keySet:先偷钥匙再开锁找值,适合眼神好的柯南。
- forEach:键值对一键打包,懒人直呼内行。
隐藏技能
- Spliterator:分手大师,把集合拆成多段并行处理(Java 8 黑科技)。
- Enumeration:爷爷辈工具,如今只剩
Vector
等古董还在用。
人类迷惑行为大赏:
🚫 用普通 for 循环暴揍LinkedList
—— 疯狂跑腿,性能扑街
🚫 在增强 for 循环里删元素 —— 系统提示:您已触发「ConcurrentModificationException」成就
List
问题集锦-List
讲一下 Java 里面 List 的几种实现,几种实现有什么不同?
ArrayList:动态数组的扛把子
- 底层结构:基于动态数组实现,初始容量 10,每次扩容 1.5 倍。
- 性能特性:
- 随机访问快(
O(1)
),但中间插入/删除需要移动元素(O(n)
)。 - 适合读多写少场景,例如缓存、配置项存储。
- 随机访问快(
- 线程安全:非线程安全,多线程环境下需手动同步。
LinkedList:链表的灵活艺术家
- 底层结构:基于双向链表实现,无容量限制。
- 性能特性:
- 头尾插入/删除快(
O(1)
),但随机访问需遍历(O(n)
)。 - 额外实现
Deque
接口,可作队列/栈使用。
- 头尾插入/删除快(
- 适用场景:高频增删操作(如消息队列)、实现栈/双向队列。
Vector:过时的安全卫士
- 底层结构:类似
ArrayList
,但所有方法通过synchronized
实现线程安全。 - 缺点:
- 性能差(同步开销大),扩容策略更激进(默认翻倍)。
- 现代开发中已被
CopyOnWriteArrayList
或Collections.synchronizedList()
取代。
- 底层结构:类似
CopyOnWriteArrayList:并发的读多写少专家
- 底层机制:写时复制(COW),写操作加锁复制新数组,读操作无锁。
- 优点:
- 读性能极高(无锁并发),适合读多写少的高并发场景(如配置中心)。
- 缺点:
- 写操作性能差(需复制整个数组),内存占用高。
关键对比与选型建议
特性 ArrayList LinkedList Vector CopyOnWriteArrayList 数据结构 动态数组 双向链表 动态数组 动态数组(写时复制) 线程安全 否 否 是(synchronized) 是(ReentrantLock) 适用场景 高频随机访问 高频增删 遗留系统 高并发读、低频写 扩容策略 1.5 倍 无需扩容 2 倍 写时动态复制
冷知识:
LinkedList
的get(index)
方法会触发「链表遍历杀」——越靠近中间越慢。
避坑指南:
- 🚫 用
ArrayList
遍历删除元素需用迭代器,否则触发ConcurrentModificationException
。- ✅ 超大数据量优先测试不同实现的耗时(如
LinkedList
头插快,但中间插入可能比ArrayList
慢)。
List 可以一边遍历一边修改元素吗?
遍历方式与修改规则
普通
for
循环- ✅ 允许修改:通过索引直接调用
set()
修改元素(如list.set(i, newValue)
)。 - ⚠️ 注意:中间插入/删除元素需手动维护索引(可能影响性能)。
- ✅ 允许修改:通过索引直接调用
增强
for
循环(foreach)- ❌ 禁止结构性修改:直接调用
add()
/remove()
会触发ConcurrentModificationException
。 - ➕ 例外:仅修改对象属性(如
obj.setName()
)不会触发异常。
- ❌ 禁止结构性修改:直接调用
迭代器(Iterator/ListIterator)
- ✅ 安全修改:通过
iterator.remove()
删除元素,listIterator.set()
修改当前元素。 - ⚠️ 限制:只能用迭代器自身方法操作,直接调用
List
的增删方法仍会触发异常。
- ✅ 安全修改:通过
线程安全 List(如
CopyOnWriteArrayList
)- ✅ 无锁遍历:写时复制机制允许遍历时修改(读旧数据,写新副本),无异常。
核心结论
- 单线程场景:优先用普通
for
循环或迭代器操作。 - 高并发场景:直接选
CopyOnWriteArrayList
,无脑避免异常。 - 开发规范:禁止在
foreach
中增删元素,Java 8+ 用removeIf()
/replaceAll()
更简洁。
- 单线程场景:优先用普通
底层原理补充(加分项)
ConcurrentModificationException
根源:迭代器内部记录修改次数(modCount
),与集合实际修改次数不一致时触发[1,2]。CopyOnWriteArrayList
通过复制新数组实现无锁读,但写操作性能较低(适合读多写少场景)。
List 如何快速删除某个指定下标的元素?
ArrayList 删除指定下标元素
- 方法:直接调用
remove(int index)
。 - 时间复杂度:
O(n)
(需移动后续元素)。 - 优化建议:高频删除中间元素可改用
LinkedList
,或反向遍历避免索引错位。
- 方法:直接调用
LinkedList 删除指定下标元素
- 方法:调用
remove(int index)
。 - 时间复杂度:
O(n)
(需遍历链表)。 - 头尾优化:用
removeFirst()
/removeLast()
可将时间降至O(1)
。
- 方法:调用
线程安全场景
- CopyOnWriteArrayList:调用
remove(int index)
会创建新数组,适合读多写少,但写性能差(O(n)
)。
- CopyOnWriteArrayList:调用
遍历时删除
- 安全方法:使用迭代器的
Iterator.remove()
,避免ConcurrentModificationException
。
- 安全方法:使用迭代器的
性能对比与选型
操作 ArrayList LinkedList CopyOnWriteArrayList 按索引删除 O(n) O(n) O(n) 适用场景 随机访问 头尾删除 高并发读场景
面试核心点:
ArrayList
和LinkedList
按索引删除的时间复杂度均为O(n)
,但LinkedList
头尾删除可优化至O(1)
。- 高频删除优先选
LinkedList
或反向遍历ArrayList
。- 线程安全场景权衡写性能,选
CopyOnWriteArrayList
或ConcurrentLinkedDeque
。
问题集锦-ArrayList
为什么 ArrayList 不是线程安全的,具体来说是哪里不安全?
非原子操作引发数据竞争
- size 递增非原子性:
size++
操作实际由读取旧值、计算新值、写入新值组成,多线程同时执行可能导致最终结果小于实际增加次数 - 元素存储覆盖:多个线程在相同索引位置执行
elementData[i] = e
,后写入的线程会覆盖前者的数据
- size 递增非原子性:
动态扩容竞态条件
- 重复扩容数据丢失:多个线程同时检测到容量不足,各自创建更大的新数组并复制数据,最终仅保留最后一个线程的扩容结果
- 扩容间隙越界风险:线程 A 扩容过程中,线程 B 仍使用旧容量索引插入数据,触发数组越界异常
内存可见性问题
- 脏读现象:线程修改数组元素后未及时同步到主内存,其他线程可能读取到修改前的旧数据
- 失效状态传播延迟:扩容后的新数组引用更新未使用同步机制,导致部分线程继续访问旧数组
迭代器快速失败机制缺陷
- 并发修改检测失效:迭代器的
expectedModCount
与集合的modCount
在多线程环境下无法保持同步 - 结构变更暴露风险:迭代过程中其他线程删除元素,可能导致当前迭代器访问到已被移除的索引位置
- 并发修改检测失效:迭代器的
复合操作非原子性
- 检查-执行间隙干扰:如
if(!contains(e)) { add(e) }
操作中,条件判断与添加动作之间可能被其他线程插入新操作 - 中间状态不一致:扩容过程中新旧数组交替阶段,其他线程可能读取到仅部分数据迁移完成的中间数组
- 检查-执行间隙干扰:如
指令重排序干扰
- 初始化顺序错乱:对象初始化过程中,JVM 优化可能打乱
elementData
赋值与size
初始化的顺序 - 写操作乱序执行:元素赋值与
size
更新的顺序被重排,导致其他线程看到size
变化但对应位置数据未更新
- 初始化顺序错乱:对象初始化过程中,JVM 优化可能打乱
ArrayList 的扩容机制说一下
扩容机制核心流程
- 当添加元素导致数组容量不足时触发扩容
- 新容量计算规则:原始容量的 1.5 倍(位运算实现:oldCapacity + (oldCapacity >> 1))
- 特殊场景处理:若计算值仍不满足需求,则直接扩容至所需的最小容量
执行步骤分解
- 创建新数组:使用
Arrays.copyOf()
生成扩容后的新数组 - 数据迁移:将旧数组元素全量复制到新数组
- 引用切换:将内部存储的数组引用指向新数组对象
- 创建新数组:使用
性能关键点
- 扩容时间复杂度:O(n)(与元素数量线性相关)
- 最佳实践:初始化时预估容量避免多次扩容
- 内存开销:每次扩容产生旧数组的垃圾对象(可能引发 GC)
容量限制机制
- 最大数组长度限制为 Integer.MAX_VALUE - 8
- 超过限制时可能抛出 OutOfMemoryError
- 虚拟机的数组对象头开销导致实际最大值小于理论值
特殊初始化场景
- 默认构造器创建的空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
- 首次添加元素时直接扩容至默认容量 10
- 显式指定初始容量为 0 时将使用特殊空数组标记
Arraylist 和 LinkedList 的区别?
底层数据结构
- ArrayList:基于动态数组(Dynamic Array)实现,内存空间连续
- LinkedList:基于双向链表(Doubly Linked List)实现,节点包含前后指针
访问性能
- ArrayList随机访问时间复杂度O(1)
- LinkedList随机访问时间复杂度O(n)
- 实测对比:10万次索引访问ArrayList耗时2ms级,LinkedList耗时秒级
插入/删除操作
- LinkedList头尾操作时间复杂度O(1)
- ArrayList尾部插入时间复杂度O(1),非尾部操作平均O(n)
- 实测对比:头部插入1万元素LinkedList耗时毫秒级,ArrayList耗时数十毫秒
内存占用
- ArrayList仅存储实际数据
- LinkedList每个节点额外存储两个指针引用
- 存储整型数据时LinkedList内存消耗约为ArrayList的5-7倍
扩容机制
- ArrayList默认扩容策略为原容量的1.5倍
- LinkedList无需预分配内存空间
线程安全性
- 两者均为非线程安全实现
- 多线程环境下需使用同步控制
应用场景
- ArrayList适用场景:
- 读多写少的配置类数据
- 需要缓存友好的连续内存访问
- LinkedList适用场景:
- 频繁首尾操作的队列场景
- 需要实现双向队列功能
- ArrayList适用场景:
ArrayList 线程安全吗?把 ArrayList 变成线程安全有哪些方法?
ArrayList 的线程安全性
- 非线程安全:默认设计不支持多线程并发操作
- 并发风险:
- 数据不一致:多线程同时修改导致元素丢失或重复
- 数组越界:动态扩容时并发操作可能触发非法索引访问
- ConcurrentModificationException:迭代过程中发生结构性修改
线程安全改造方法
Collections.synchronizedList
- 实现方式:通过装饰器模式对所有方法添加对象级锁
- 特点:
- 读写操作均加锁,适用于中等并发场景
- 迭代器遍历需显式同步
- 组合操作需外部同步控制
CopyOnWriteArrayList
- 核心机制:写操作复制新数组,读操作访问旧数组
- 优势:
- 无锁读操作,适合读多写少场景
- 数据最终一致性保证
- 缺陷:
- 内存占用随写操作次数线性增长
- 不适用实时性要求高的场景
手动同步锁
- 实现要点:
- 自定义锁对象控制关键代码段
- 缩小同步代码块范围提升性能
- 适用场景:
- 需要精细化控制锁粒度
- 高并发环境下的特定操作优化
- 实现要点:
Vector(遗留方案)
- 特点:
- 方法级同步锁导致性能瓶颈
- 扩容策略与 ArrayList 不同(默认 2 倍 vs 1.5 倍)
- 使用建议:仅用于兼容旧系统,新项目避免使用
- 特点:
方案对比与选型
- 性能维度:
- 高频读:CopyOnWriteArrayList > 手动锁 > synchronizedList > Vector
- 高频写:手动锁 > synchronizedList > Vector > CopyOnWriteArrayList
- 内存维度:CopyOnWriteArrayList 内存消耗最大
- 开发成本:synchronizedList 实现成本最低
- 性能维度:
底层原理扩展
- JVM 内存模型:
- 未同步操作可能违反 happens-before 原则
- volatile 关键字保证可见性但无法解决原子性问题
- 锁优化技术:
- 偏向锁 → 轻量级锁 → 重量级锁的升级过程
- 自旋锁减少线程切换开销
- 硬件支持:
- CAS 操作依赖 CPU 的 cmpxchg 指令
- 内存屏障保证指令执行顺序
- JVM 内存模型:
ArrayList 和 LinkedList 的应用场景?
ArrayList 核心应用场景
- 高频随机访问需求
- 基于动态数组结构,通过索引访问元素时间复杂度为 O(1)
- 典型场景:电商商品信息存储、数据分析中的快速遍历统计
- 尾部操作密集型任务
- 尾部插入/删除时间复杂度为 O(1)
- 典型场景:日志追加存储、传感器数据顺序采集
- 内存敏感型场景
- 连续内存空间存储,无额外指针开销
- 典型场景:百万级配置参数存储、移动端内存限制场景
- 高频随机访问需求
LinkedList 核心应用场景
- 频繁中间/头部操作
- 插入/删除时间复杂度为 O(1)
- 典型场景:聊天消息队列、文本编辑器撤销链
- 特殊数据结构实现
- 原生支持双端队列操作
- 典型场景:任务调度插队系统、LRU 缓存算法
- 动态伸缩需求场景
- 无需预分配内存,按需动态增长
- 典型场景:网络数据包接收、游戏事件处理器链
- 频繁中间/头部操作
对比选型决策维度
- 选择 ArrayList 的明确条件
- 主要操作为索引访问或尾部增删
- 数据规模可预估且波动范围较小
- 需要利用 CPU 缓存空间局部性原理
- 选择 LinkedList 的明确条件
- 主要操作为非尾部位置增删
- 需要实现队列/栈等数据结构
- 内存资源充足但需规避扩容开销
- 选择 ArrayList 的明确条件
性能优化注意事项
- ArrayList 使用禁忌
- 避免在循环中频繁执行
contains()
+add()
组合操作 - 初始化时通过
ensureCapacity()
预分配空间
- 避免在循环中频繁执行
- LinkedList 使用禁忌
- 避免通过索引遍历(时间复杂度 O(n^2))
- 警惕超大数据量下的内存占用激增问题
- ArrayList 使用禁忌
高级组合使用策略
- 冷热数据分离存储
- 高频修改数据使用 LinkedList,只读数据使用 ArrayList
- 分段混合存储
- 将大数据集拆分为多个 ArrayList 子块,用链表维护块关系
- 动态切换机制
- 根据运行时监控数据自动切换 List 实现类型
- 冷热数据分离存储
线程安全的 List, CopyonWriteArraylist 是如何实现线程安全的
写时复制核心机制
- 所有写操作前先复制当前数组生成新副本
- 修改操作仅在新副本上执行,完成时通过原子操作替换数组引用
- 读操作始终访问原数组,实现读写操作物理隔离
锁控制实现
- 使用
ReentrantLock
保证写操作原子性 - 锁粒度控制在修改操作全过程(包含复制、修改、替换引用)
- 确保同一时刻只有一个线程执行写操作
- 使用
内存可见性保障
- 数组引用声明为
volatile
- 新数组引用替换后对所有线程立即可见
- 写操作完成后旧数组立即不可变
- 数组引用声明为
迭代器弱一致性
- 迭代器基于创建时的数组快照工作
- 遍历过程中不会检测并发修改
- 可能访问到历史版本数据但保证结构完整性
性能特征
- 读操作完全无锁,时间复杂度 O(1)
- 写操作时间复杂度 O(n)(需完整复制数组)
- 单次写操作内存开销与数据量成正比
适用场景边界
- 最佳场景:读操作频率高于写操作 100 倍以上
- 禁忌场景:高频写入大数据量集合(如实时日志记录)
- 典型应用:白名单/黑名单配置、事件监听器列表
ArrayList 与 Vector 的区别
初始化
ArrayList 空构造函数是构造的空的集合,在添加元素时初始化默认为 10 容量的数据,Vector 空构造函数初始就分配了 10 个长度的空间。线程安全性
ArrayList 是非线程安全的,Vector 通过 synchronized 关键字实现了方法级别的同步。在多线程场景中直接使用 Vector 更安全,但会带来额外性能开销。扩容机制
- Vector 默认扩容为原容量的 2 倍,支持自定义扩容增量。
- ArrayList 默认扩容为原容量的 1.5 倍,且不支持自定义策略。
两者的动态扩容均会导致内存占用增加,但 Vector 的激进扩容可能更适合存储大量数据的场景。
性能差异
- ArrayList 无同步锁,单线程下操作效率更高。
- Vector 的同步机制会导致多线程竞争时的性能下降。若需线程安全且高效,建议使用同步包装类或并发容器。
历史背景
- Vector 是 Java 早期基于 Dictionary 类实现的遗留类。
- ArrayList 是 Java 1.2 引入的现代化集合框架组件,设计更轻量且符合接口抽象原则。
适用场景
- ArrayList:单线程环境、频繁随机访问、无需同步的高性能场景。
- Vector:需线程安全且性能要求不高的多线程场景。实际开发中更推荐通过并发包实现线程安全。
Map
HashMap
HashMap 实现原理
底层数据结构
- HashMap 的底层由数组 + 链表 + 红黑树组成:
- 数组(哈希桶):初始容量为 16,存储链表头节点或红黑树根节点,每次扩容翻倍。
- 链表:解决哈希冲突,同一索引的键值对以链表形式存储。
- 红黑树(JDK 1.8+):链表长度超过 8 且数组容量 ≥ 64 时,链表转为红黑树,查询复杂度从 O(n) 优化为 O(log n)。
- HashMap 的底层由数组 + 链表 + 红黑树组成:
哈希函数与索引计算
- 哈希扰动:通过
hash(key) = (h = key.hashCode()) ^ (h >>> 16)
混合高低位,降低冲突概率。 - 索引定位:
index = (n - 1) & hash
(n
为数组长度且保持 2 的幂次方),位运算替代取模提升效率。
- 哈希扰动:通过
核心操作流程
- Put 操作:
- 计算哈希值定位索引。
- 若桶为空则插入新节点;否则遍历链表/树:
- 存在相同 key 时覆盖旧值(通过
equals
判断)。 - 不存在则插入链表末尾或红黑树。
- 存在相同 key 时覆盖旧值(通过
- 触发扩容条件:元素数量 ≥ 容量 × 负载因子(默认 0.75)。
- Get 操作:
- 计算哈希值定位索引,遍历链表/树匹配 key。
- 返回 value 或
null
。
- Put 操作:
哈希冲突解决机制
- 链地址法:冲突键值对存入同一桶的链表或红黑树。
- 扩容:负载因子超标时数组翻倍,重新散列元素以减少冲突。
线程安全问题
- 多线程同时修改结构(如 put/remove)可能导致数据不一致或死循环(JDK 1.7 头插法)。
- 解决方案:使用
ConcurrentHashMap
或Collections.synchronizedMap
包装。
JDK 版本差异
- JDK 1.7:头插法链表,多线程扩容易引发死循环。
- JDK 1.8:尾插法,引入红黑树优化,降低并发风险。
自定义 Key 的设计要求
- 必须重写
hashCode()
和equals()
:确保相同逻辑的 key 哈希值一致且等价,否则数据可能丢失。 - 示例:未重写时,内容相同的两个对象作为 key 会被视为不同,导致
get
返回null
。
- 必须重写
性能优化建议
- 初始容量:预估数据量设置(如
new HashMap<>(1000)
),避免频繁扩容。 - 负载因子:内存充足时可降低负载因子以减少冲突。
- 哈希分散性:优化
hashCode()
方法,使键分布均匀。
- 初始容量:预估数据量设置(如
详见 HashMap。
HashMap 是线程安全的吗?
线程安全性结论
HashMap 不是线程安全的类。多线程环境下直接使用可能导致:- 数据丢失(多个线程 put 时覆盖写入)。
- 数据不一致(读取到中间状态)。
- 死循环(JDK 1.7 头插法扩容形成环形链表)。
- 迭代器失效(遍历时修改结构抛出
ConcurrentModificationException
)。
非线程安全的核心原因
- 操作非原子性:put 和扩容涉及多步骤且未加锁。
- 共享资源竞争:多线程同时修改同一哈希桶(链表/红黑树)。
- JDK 1.7 头插法缺陷:扩容时链表顺序反转易形成环。
线程安全替代方案
- ConcurrentHashMap:
- JDK 1.7 使用分段锁,JDK 1.8+ 改用 CAS +
synchronized
锁桶头节点。 - 高并发场景首选,性能优异。
- JDK 1.7 使用分段锁,JDK 1.8+ 改用 CAS +
- Collections.synchronizedMap:
- 通过
synchronized
包装 HashMap 所有方法。 - 简单但性能较差,适合低并发场景。
- 通过
- Hashtable(已过时):
- 全局锁机制导致性能极低,不推荐使用。
- ConcurrentHashMap:
面试回答要点
- 明确结论:HashMap 非线程安全。
- 核心问题:数据竞争导致覆盖/丢失,JDK 1.7 死循环问题。
- 解决方案:优先推荐 ConcurrentHashMap,次选 SynchronizedMap。
- 版本差异:JDK 1.8 尾插法解决死循环,但数据竞争仍存在。
HashMap 的 put 过程
哈希值计算
- 调用
key.hashCode()
获取原始哈希值,通过扰动函数(h = key.hashCode()) ^ (h >>> 16)
混合高低位,降低冲突概率。 - 若 key 为 null,哈希值固定为 0。
- 调用
定位桶索引
- 计算索引:
index = (n - 1) & hash
,其中n
为数组长度(保持 2 的幂次方)。 - 若数组未初始化(首次插入),调用
resize()
初始化容量为 16,负载因子为 0.75。
- 计算索引:
哈希冲突处理
- 空桶:直接创建新节点存入数组。
- 非空桶:
- 链表:遍历链表查找相同 key(
equals
匹配):- 存在相同 key:覆盖旧 value。
- 不存在:插入链表末尾。链表长度 ≥ 8 且数组容量 ≥ 64 时转为红黑树。
- 红黑树:调用
putTreeVal()
插入或更新节点。
- 链表:遍历链表查找相同 key(
扩容触发与迁移
- 当元素数量超过阈值(容量 × 负载因子)时触发扩容,新容量为旧容量的 2 倍。
- 迁移规则:根据
(e.hash & oldCap)
判断节点分配到原索引或原索引 + 旧容量的位置。
返回结果与状态更新
- key 存在时返回旧 value,否则返回 null。
- 更新
modCount
计数器,用于迭代器的快速失败检测。
HashMap get 过程
哈希值计算与索引定位
- 计算 key 的哈希值:
key.hashCode()
经扰动函数(h = key.hashCode()) ^ (h >>> 16)
混合高低位。 - 若 key 为 null,哈希值固定为 0。
- 计算索引:
index = (n - 1) & hash
(n
为数组长度,保持 2 的幂次方)。
- 计算 key 的哈希值:
桶位元素检查
- 根据索引定位数组中的桶,若桶为空(
table[index] == null
),直接返回 null。 - 若桶非空,检查首节点的哈希值和 key 是否匹配(通过
==
或equals
)。若匹配则返回 value。
- 根据索引定位数组中的桶,若桶为空(
链表或红黑树遍历
- 链表:首节点不匹配时遍历链表,通过
equals
方法逐一比对 key,找到则返回 value,否则返回 null。 - 红黑树(JDK 1.8+):调用
getTreeNode()
递归查找树节点,基于哈希值和 key 大小决定子树方向。
- 链表:首节点不匹配时遍历链表,通过
时间复杂度与性能
- 无冲突时 O(1),链表冲突时 O(n),红黑树优化后 O(log n)。
注意事项
- 必须正确重写
hashCode()
和equals()
以确保 key 匹配。 - JDK 1.8 红黑树优化长链表查询效率。
- 迭代器依赖
modCount
检测并发修改,触发ConcurrentModificationException
。
- 必须正确重写
HashMap 调用 get 方法一定安全吗?
纯读场景的理论安全性
- 若所有线程仅执行
get()
且未发生扩容,理论上是线程安全的。此时get()
不修改结构,仅遍历现有节点。
- 若所有线程仅执行
并发写入的潜在风险
- 死循环(JDK 1.7):扩容时链表头插法可能形成环,
get()
遍历陷入死循环。 - 中间状态读取:扩容迁移过程中可能读到未完成迁移的旧数组或部分数据。
- 数据不一致:并发
put()
导致覆盖写入或扩容未完成时get()
返回 null 或旧值。
- 死循环(JDK 1.7):扩容时链表头插法可能形成环,
JDK 版本差异影响
- JDK 1.7:头插法扩容存在成环风险,
get()
高并发下可能死循环。 - JDK 1.8+:尾插法避免成环,但
get()
仍可能读取到扩容中间状态。
- JDK 1.7:头插法扩容存在成环风险,
安全实践建议
- 纯读场景:初始化后不修改 HashMap 可视为安全。
- 读写混合场景:使用
ConcurrentHashMap
或Collections.synchronizedMap
包装。 - 预分配容量:减少扩容触发概率。
根本原因与验证
- 问题根源:HashMap 未对结构性修改(扩容)与读取操作做同步控制。
- 复现方法:压力测试工具模拟并发读写,观察 CPU 占用或异常抛出。
HashMap 一般用什么做 Key?为啥 String 适合做 Key 呢?
常用 Key 类型
- String:最常用,因其不可变性和哈希计算高效。
- Integer/Enum:基于数值的哈希值,简单高效。
- 自定义不可变类:需严格实现
hashCode()
和equals()
。 - 其他包装类(如 Long):适用于简单键值场景。
String 适合作为 Key 的核心原因
- 不可变性:
- 创建后不可修改,哈希值稳定,避免数据丢失。
- 哈希计算高效:
hashCode()
使用 31 的质数乘法,分布均匀且结果缓存复用。
- 字符串常量池优化:
- 相同字面量共享内存,加速比较(通过
==
判断引用相等性)。
- 相同字面量共享内存,加速比较(通过
- 完善的
equals()
实现:- 字符逐位比对,性能高效且逻辑严谨。
- 语义清晰:
- 适用于配置、URL 等场景,易读且兼容性强。
- 不可变性:
使用 Key 的注意事项
- 优先选择不可变对象:可变对象可能导致哈希值变化,引发数据不一致。
- 正确重写
hashCode()
和equals()
:- 确保逻辑一致,避免哈希表定位失败。
- 避免复杂对象:增加冲突概率且维护成本高。
为什么 HashMap 要用红黑树而不是平衡二叉树
插入与删除性能优势
- 红黑树插入/删除最多需 2 次旋转,平衡二叉树(如 AVL 树)可能需多次旋转。红黑树允许局部不平衡(最长路径 ≤ 2 倍最短路径),降低调整频率,适合高频更新场景。
查询效率的折中
- 平衡二叉树查询略优(树更平衡),但 HashMap 树规模小(阈值 8,树高 ≤ 3),红黑树 O(log n) 效率已接近理想值。
计算开销与内存优化
- 红黑树通过颜色标记替代高度差计算,减少 CPU 开销。元素减少到 6 时转回链表,避免内存浪费。
工程实现的成熟性
- 红黑树在 Linux 内核、Java TreeMap 等广泛应用,实现经过长期验证。平衡二叉树旋转逻辑复杂,维护成本高。
综合性能平衡
- 红黑树在插入、删除、查询间平衡更优,适合 HashMap 动态存储需求(如扩容与数据迁移)。
HashMap key 可以为 null 吗?
允许 null 键的机制
- key 为 null 时,HashMap 通过
hash()
返回哈希值 0,避免空指针异常。 - null 键存储在数组索引 0 的位置,且仅允许存在一个(键唯一性保证)。
- key 为 null 时,HashMap 通过
设计原因与场景
- 灵活性:支持表示“未知键”或配置中的默认值。
- 单线程优化:设计未考虑多线程下 null 键的二义性(如
get(key)
返回 null 时无法区分键是否存在)。
与其他容器的对比
- Hashtable:禁止 null 键/值,直接调用
key.hashCode()
会抛空指针异常。 - ConcurrentHashMap:禁止 null 键/值,因多线程下无法消除二义性风险,且实用价值有限。
- Hashtable:禁止 null 键/值,直接调用
使用注意事项
- 线程安全:多线程操作 null 键需同步控制,否则可能导致数据覆盖或逻辑错误。
- 代码可读性:建议用 Optional 类或明确业务语义替代,避免过度依赖 null 键。
列举 HashMap 在多线程下可能会出现的问题?
数据覆盖与丢失
多线程同时插入相同哈希桶时,可能因同时检测空桶导致数据覆盖。并发扩容时节点迁移遗漏可能造成数据丢失。死循环
(1)JDK 1.7 及之前:头插法扩容导致链表成环,触发get()
或put()
操作时陷入无限循环,CPU 占用率飙升。
(2)JDK 1.8 之后:树化扩容时,也会出现死循环常见,不过概率很低。ConcurrentModificationException 异常
迭代器遍历期间其他线程修改结构,触发快速失败机制抛出异常。脏读与逻辑错误
读取到扩容迁移中的中间状态数据,get(key)
返回 null 时无法区分键是否存在或值为 null。扩容性能下降
多线程重复触发扩容导致哈希重计算和节点迁移,增加 CPU 负载。JDK 1.8 扩容时仍需锁定当前桶引发线程阻塞。结构破坏与内存泄漏
并发修改可能导致链表断裂或树结构失衡,节点引用错误可能引发内存无法回收。
HashMap 的扩容机制介绍一下
扩容触发条件
- 元素数量超过阈值(阈值 = 容量 × 负载因子,默认 0.75)。
- 链表长度 ≥8 且数组长度 <64 时优先扩容而非树化。
扩容流程
- 新数组创建:容量翻倍(保持 2 的幂次方)。
- 数据迁移:
- 链表拆分(JDK8+):采用高低位分组策略,将链表拆分为低位链(原索引)和高位链(原索引 + 旧容量)。
- 红黑树处理:节点数 ≤6 时退化为链表,否则拆分迁移。
- 尾插法优化:JDK8 用尾插法替代头插法,避免多线程成环问题。
性能优化
- 位运算
(n-1) & hash
替代取模运算,提升计算效率。 - 默认负载因子 0.75 平衡空间利用率与冲突概率。
- 初始化时建议预估容量(如
new HashMap<>(预估容量/0.75 +1)
)。
- 位运算
多线程风险
- 并发插入或扩容可能导致数据覆盖、脏读。
- JDK7 头插法扩容会引发死循环,JDK8 尾插法彻底解决。
- 推荐使用
ConcurrentHashMap
保证线程安全。
参数调优与监控
- 强制禁用树化:通过 JVM 参数
-Djava.util.HashMap.treeifyThreshold=2147483647
(牺牲查询性能)。 - 负载因子调整:内存敏感场景可调高(如 0.9),时间敏感场景调低(如 0.6)。
- 扩容性能监控:关注 GC 日志中
HashMap.resize()
耗时,优化容量规划。
- 强制禁用树化:通过 JVM 参数
HashMap 的大小为什么是 2 的 n 次方大小呢?
位运算优化与计算效率
- 容量为 2 的 n 次方时,
(n-1) & hash
等价于hash % n
,但位运算性能更优(避免除法运算开销)。 - 示例:当容量为 16(二进制
10000
)时,n-1 = 15
(二进制1111
),位运算直接取哈希值低 4 位。
- 容量为 2 的 n 次方时,
哈希分布均匀性
- 低位掩码(
n-1
全 1)确保哈希值的所有位参与索引计算,减少哈希碰撞概率。 - 若哈希值高位不同但低位相同,仍可能分配到同一桶,但掩码机制比非 2 幂次方容量更均衡。
- 低位掩码(
扩容迁移高效性
- 新容量为旧容量 2 倍时,迁移只需判断哈希值最高有效位:
- 最高位为 0:新索引 = 原索引。
- 最高位为 1:新索引 = 原索引 + 旧容量。
- 示例:旧容量 16,哈希值
10101
的索引从 5 变为 21(原索引 + 16)。
- 新容量为旧容量 2 倍时,迁移只需判断哈希值最高有效位:
容量自动对齐机制
- 通过
tableSizeFor()
方法将用户输入容量对齐到最小 2 的幂次方。例如输入 21 时调整为 32。 - 实现方式:通过位右移和或操作填充最高位后的所有位为 1,再加 1 得到目标容量。
- 通过
数据结构兼容性
- 链表转红黑树、拆分迁移等操作依赖容量为 2 的幂次方,例如高低位拆分逻辑与位掩码计算直接关联。
- 容量非 2 的幂次方时,扩容后无法保证高低位拆分规则的简洁性,增加迁移复杂度。
往 HashMap 存 20 个元素,会扩容几次?
初始容量与扩容触发条件
- 默认初始容量为 16,负载因子为 0.75,扩容阈值为
容量 × 0.75
。 - 首次
put()
时初始化容量为 16(不视为扩容)。 - 当元素数量超过阈值(如 16 × 0.75 = 12)时触发扩容。
- 默认初始容量为 16,负载因子为 0.75,扩容阈值为
扩容过程分析(默认参数下存入 20 个元素)
- 第一次扩容:存入第 13 个元素时触发,容量从 16 扩容至 32,新阈值更新为 32 × 0.75 = 24。
- 后续操作:存入第 14~20 个元素时,元素数量始终 ≤24,不再触发扩容。
特殊情况说明
- 链表树化:若某链表长度达到 8 但数组长度 <64,会优先扩容而非树化,但仅计入一次扩容。
- 手动指定容量:例如
new HashMap<>(32)
时,阈值 = 32 × 0.75 = 24,存入 20 个元素不会触发扩容。
总结
- 默认场景下存入 20 个元素仅触发 1 次扩容(容量 16 → 32)。
- 扩容次数由元素数量是否超过阈值决定,与链表/树结构无关。
说说 HashMap 的负载因子
负载因子的定义与作用
- 负载因子是 HashMap 中元素数量与容量的比率,用于控制扩容时机。
- 当元素数量超过
容量 × 负载因子
时触发扩容(容量翻倍并 rehash)。
默认值 0.75 的权衡
- 空间与时间平衡:
- 过高(如 1.0)导致哈希冲突增多,查询效率降低。
- 过低(如 0.5)导致扩容频繁,空间利用率下降。
- 实践验证:0.75 是多数场景下减少冲突与避免频繁扩容的最优解。
- 空间与时间平衡:
调整策略
- 高并发场景:降低负载因子(0.5~0.6),减少冲突提升查询效率。
- 内存敏感场景:提高负载因子(0.8~0.9),减少扩容频率但容忍更高冲突。
- 初始化优化:通过构造函数指定(如
new HashMap<>(16, 0.6f)
),或按预估容量 / 负载因子 +1
计算初始容量。
对性能的影响
- 查询效率:负载因子越高,链表/红黑树遍历时间可能增加。
- 扩容成本:负载因子越低,rehash 操作越频繁,CPU 消耗增加。
设计依据
- 源码注释明确 0.75 是空间利用率与时间复杂度的折衷选择。
- 扩容机制(如
(n-1) & hash
)需与负载因子设计兼容。
HashTable
HashTable 底层实现原理
底层数据结构
- 基于数组 + 链表实现,数组元素为
Entry
对象(包含 key、value、hash 和 next 指针)。
- 基于数组 + 链表实现,数组元素为
哈希函数与索引计算
- 哈希值计算:
key.hashCode()
,再通过(hash & 0x7FFFFFFF) % 数组长度
确定索引。 0x7FFFFFFF
确保哈希值为非负数,避免索引为负。
- 哈希值计算:
线程安全实现
- 所有公共方法(如 put、get)添加
synchronized
关键字,保证单线程操作。 - 高并发性能差,推荐改用
ConcurrentHashMap
。
- 所有公共方法(如 put、get)添加
扩容机制
- 初始容量 11,负载因子 0.75,阈值 = 容量 × 负载因子。
- 触发扩容后,新容量 = 旧容量 × 2 + 1,并重新哈希所有元素。
哈希冲突处理
- 链地址法:冲突的 Entry 对象以链表形式存储,新元素插入链表头部(JDK 1.8 前)。
- 查找时遍历链表,时间复杂度 O(n)。
特殊限制
- key 和 value 均不允许为 null,否则抛出
NullPointerException
。 - 继承自
Dictionary
类(已过时),与HashMap
(继承AbstractMap
)不同。
- key 和 value 均不允许为 null,否则抛出
HashTable 线程安全是怎么实现的?
方法级同步锁
- 所有公共方法(如 put、get)使用
synchronized
修饰,确保同一时间仅一个线程操作 HashTable。
- 所有公共方法(如 put、get)使用
内存可见性保障
synchronized
保证锁释放时将本地修改刷新到主内存,获取锁时读取最新值,避免数据不可见问题。
扩容机制
- 初始容量 11,负载因子 0.75,扩容阈值为
容量 × 负载因子
。触发扩容后,新容量 = 旧容量 × 2 +1。
- 初始容量 11,负载因子 0.75,扩容阈值为
与其他容器的对比
- ConcurrentHashMap:分段锁或 CAS 机制支持并发读写,性能更高。
- synchronizedMap:对普通 HashMap 加对象级锁,性能与 HashTable 类似。
使用限制与性能问题
- 禁止 null 键/值,操作需显式处理空值。
- 全表锁导致读写互斥,高并发场景性能差,推荐改用 ConcurrentHashMap。
ConcurrentHashMap
ConcurrentHashMap 怎么实现的?
线程安全机制
- JDK 1.7 分段锁:将哈希表划分为多个 Segment,每个段独立加锁,支持不同段并发操作。
- JDK 1.8 细粒度锁 + CAS:取消分段锁,对哈希桶(Node)加锁。插入时先尝试 CAS 无锁插入头节点,冲突时用
synchronized
加锁处理链表或红黑树。
数据结构与扩容
- 数组 + 链表/红黑树:链表长度 ≥8 且数组容量 ≥64 时转为红黑树,查询效率提升至 O(log n)。
- 渐进式扩容:容量翻倍,多线程协作迁移数据,迁移期间读写可同时访问新旧数组。用 ForwardingNode 标记已迁移的桶。
CAS 与原子操作
- 无锁初始化:通过 CAS 确保单线程初始化哈希表。
- 原子更新控制:插入空桶时 CAS 写入头节点,扩容时通过 sizeCtl 变量协调线程任务分配。
内存可见性保障
- 哈希桶数组声明为 volatile,确保读取最新状态。
- 节点 next 字段使用 volatile 修饰,防止链表遍历时数据不一致。
性能对比
- 相比 Hashtable:ConcurrentHashMap 支持并发读写不同桶,读操作无锁(JDK 1.8),吞吐量更高。
- 相比 synchronizedMap:细粒度锁减少竞争,支持迭代期间并发修改。
分段锁怎么加锁的?
定位分段与锁对象
- 键的哈希值经过两次散列运算:
- 第一次散列定位到具体的 Segment 分段(如
hash % concurrencyLevel
,默认并发级别 16)。 - 第二次散列在 Segment 内部定位到具体的 HashEntry 桶(链表头节点位置)。
- 第一次散列定位到具体的 Segment 分段(如
- 每个 Segment 继承自
ReentrantLock
,包含独立的 HashEntry 数组和锁状态。
- 键的哈希值经过两次散列运算:
获取分段锁流程
- 写操作(如 put)调用
Segment.lock()
获取该分段的可重入锁。 - 不同分段的操作并行执行(如操作 Segment[0] 和 Segment[3] 互不阻塞)。
- 若分段被占用,当前线程阻塞直到锁释放。
- 写操作(如 put)调用
操作与释放锁
- 加锁后在 Segment 的 HashEntry 链表中执行插入/删除/更新操作。
- 操作完成后调用
Segment.unlock()
释放锁,其他线程可竞争该锁。
并发性能优势
- 不同分段操作完全并行,仅同一分段内触发锁竞争。
- 读操作无锁,依赖
volatile
修饰的 HashEntry 字段(如 value)保证可见性。
分段锁是可重入的吗?
可重入性实现
- JDK 1.7 的 Segment 锁继承自
ReentrantLock
,具备可重入特性。 - 内部维护计数器(
state
变量):同一线程多次获取锁时计数器递增,释放时递减,归零后释放资源。
- JDK 1.7 的 Segment 锁继承自
运作原理
- 线程持有标记:
ReentrantLock
记录当前持有锁的线程,同一线程重复获取时直接累加计数器。 - 释放规则:每次
lock()
必须对应一次unlock()
,否则锁无法完全释放。
- 线程持有标记:
对比非可重入锁
- 非可重入锁(如简单互斥锁)无法识别持有者,同一线程重复获取会死锁。
- 分段锁的可重入性避免此问题,支持递归或嵌套调用。
实际意义
- 避免死锁:例如
put
操作内部调用其他需加锁的逻辑时,线程可重入同一锁。 - 简化编程:无需避免同一线程的重复加锁行为,降低代码复杂度。
- 避免死锁:例如
ConcurrentHashMap 用了悲观锁还是乐观锁?
JDK 1.7 分段锁(悲观锁)
- 基于
ReentrantLock
实现,每个 Segment 独立加锁。 - 写操作需获取分段独占锁,同一分段内线程竞争阻塞,不同分段可并行操作。
- 基于
JDK 1.8 混合锁机制
- CAS(乐观锁):
- 初始化数组、插入空桶头节点时使用 CAS 无锁操作。
- 扩容时通过 CAS 更新
sizeCtl
协调多线程任务。
- synchronized(轻量级悲观锁):
- 哈希冲突时(链表/红黑树操作)对当前桶加锁,锁粒度细化到单个桶。
- 读操作无锁:依赖
volatile
修饰的Node.val
和next
字段保证可见性。
- CAS(乐观锁):
锁选择逻辑与优势
- 低冲突场景(如空桶插入)优先使用 CAS,避免线程阻塞。
- 高冲突场景(如链表修改)使用
synchronized
保证数据一致性。 - 混合机制减少上下文切换和自旋开销,提升并发性能。
HashMap 和 HashTable 有什么不一样的?HashMap 一般怎么用?
HashMap 与 HashTable 的核心差异
- 线程安全:
- HashTable 所有方法使用
synchronized
修饰,性能低。 - HashMap 非线程安全,高并发下推荐
ConcurrentHashMap
。
- HashTable 所有方法使用
- null 支持:
- HashTable 禁止 null 键/值,否则抛出异常。
- HashMap 允许一个 null 键和多个 null 值。
- 继承结构:
- HashTable 继承
Dictionary
类(已过时)。 - HashMap 继承
AbstractMap
,实现Map
接口。
- HashTable 继承
- 性能与扩容:
- HashTable 初始容量 11,扩容公式为
旧容量 × 2 +1
。 - HashMap 初始容量 16,翻倍扩容,哈希桶 + 链表/红黑树结构(JDK 1.8+)。
- HashTable 初始容量 11,扩容公式为
- 迭代器:
- HashTable 使用
Enumerator
,不支持快速失败。 - HashMap 使用
Iterator
,支持快速失败机制。
- HashTable 使用
- 线程安全:
HashMap 的典型用法
- 基本操作:
HashMap<String, Integer> map = new HashMap<>(); map.put("key", 100); // 添加键值对 int value = map.get("key"); // 获取值 map.remove("key"); // 删除键值对
- 遍历方式:
entrySet()
遍历键值对:for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
keySet()
或values()
单独遍历键或值。
- 嵌套结构:
HashMap<String, HashMap<String, Integer>> nestedMap = new HashMap<>(); nestedMap.put("A", new HashMap<>()).put("B", 200);
- 性能优化:
- 初始化时预估容量:
new HashMap<>(预期容量/0.75 +1)
。 - 确保键对象正确实现
hashCode()
和equals()
。
- 初始化时预估容量:
- 适用场景:
- 缓存系统(快速存取)。
- 配置管理(键值映射)。
- 数据统计(如词频统计)。
- 基本操作:
总结建议
- 单线程优先选择 HashMap,高并发用 ConcurrentHashMap。
- 避免滥用 null 键/值,提高代码健壮性。
- 合理初始化容量以减少扩容开销。
已经用了 synchronized,为什么还要用 CAS 呢?
性能优化与场景适配
- synchronized 的阻塞代价:线程获取不到锁时会阻塞,导致上下文切换和内核态切换,高并发下吞吐量下降。
- CAS 的无锁优势:在低冲突场景(如空桶插入)通过自旋重试避免阻塞,性能更高。
锁粒度与并发效率
- synchronized 的粗粒度问题:锁定整个对象或代码块,强制串行化所有操作。
- CAS 的细粒度控制:针对单个变量原子操作(如
AtomicInteger
计数更新),不影响其他变量或操作。
功能互补性
- CAS 的局限性:无法处理复杂逻辑(如链表遍历+修改)或多变量操作,需 synchronized 保证结构一致性。
- synchronized 的优化:Java 1.6 后引入偏向锁、轻量级锁,但 CAS 可减少锁升级概率。
混合锁机制的实践
- 协同使用场景:
- 低冲突优先 CAS(如空桶插入)。
- 高冲突退化为 synchronized(如链表/红黑树修改)。
- 设计哲学:乐观并发控制(CAS)与悲观锁(synchronized)结合,平衡性能与一致性。
- 协同使用场景:
说一下 HashMap、HashTable 和 ConcurrentHashMap 的区别
线程安全与锁机制
- HashTable:使用 synchronized 锁所有方法,锁粒度粗(整表锁定),多线程性能差。
- HashMap:非线程安全,单线程性能最优,多线程操作可能导致数据不一致或死循环。
- ConcurrentHashMap:JDK 1.7 分段锁,JDK 1.8 CAS + synchronized 细粒度锁(锁定单个桶或链表节点),高并发性能优。
null 键值支持
- HashTable:禁止 null 键/值,插入 null 抛出 NullPointerException。
- HashMap:允许一个 null 键和多个 null 值。
- ConcurrentHashMap:禁止 null 键/值,避免并发下语义歧义(如无法区分“键不存在”与“值为 null”)。
性能对比
- 单线程:HashMap > ConcurrentHashMap > HashTable。
- 多线程:ConcurrentHashMap 无锁读 + 细粒度写锁,性能远高于 HashTable。
数据结构与冲突处理
- HashTable:数组 + 链表,无红黑树优化,冲突链表可能过长。
- HashMap:JDK 1.8+ 数组 + 链表/红黑树(链表长度 ≥8 且数组容量 ≥64 时树化)。
- ConcurrentHashMap:同 HashMap,但线程安全机制保证并发操作。
迭代器行为
- HashTable:强一致性,迭代时修改抛 ConcurrentModificationException。
- HashMap:fail-fast 快速失败,多线程修改可能抛异常。
- ConcurrentHashMap:弱一致性,允许迭代期间修改,不保证实时数据。
适用场景
- HashTable:已过时,仅用于遗留系统或低并发场景。
- HashMap:单线程场景(如本地缓存、配置存储)。
- ConcurrentHashMap:高并发场景(如分布式缓存、计数器)。
Set
Set 集合有什么特点?如何实现 key 无重复的?
Set 集合核心特点
- 无序性:不保证存储顺序(如 HashSet),部分实现(如 LinkedHashSet)通过链表维护插入顺序。
- 唯一性:元素必须唯一,重复元素自动过滤。
- 允许 null 值:多数实现(如 HashSet)允许一个 null,ConcurrentHashSet 等线程安全实现禁止 null。
- 动态扩容:无固定大小限制,底层通过哈希表或平衡树管理容量。
实现 key 无重复的机制
- 哈希表与 hashCode():
- 添加元素时计算
hashCode()
,哈希函数定位存储位置。 - 哈希位置为空则存储;冲突时触发链表/红黑树处理。
- 添加元素时计算
- equals() 精确判重:
- 哈希冲突时调用
equals()
判断元素是否相同,相同则拒绝添加。 - 自定义类需重写
hashCode()
和equals()
,否则基于内存地址判重。
- 哈希冲突时调用
- 哈希表与 hashCode():
不同实现类特性对比
- HashSet:基于哈希表,查询 O(1),无序,允许 null。
- LinkedHashSet:维护插入顺序链表,适合需保留顺序的场景。
- TreeSet:基于红黑树,元素按自然顺序或比较器排序,查询 O(log n)。
- ConcurrentHashSet:线程安全(如包装 ConcurrentHashMap),禁止 null。
应用场景
- 去重操作:快速过滤重复元素(如日志去重)。
- 集合运算:高效计算交集、并集、差集(如权限比对)。
- 缓存唯一数据:存储唯一标识符或配置项(如用户 ID 缓存)。
有序的 Set 是什么?记录插入顺序的集合是什么?
有序的 Set 实现
- LinkedHashSet:基于哈希表 + 双向链表,维护元素插入顺序。迭代顺序与添加顺序一致,适合需要保留插入顺序的去重场景(如操作日志记录)。
- TreeSet:基于红黑树,元素按自然顺序(如数值大小)或自定义比较器排序,适合需要排序查询的场景(如排行榜)。
记录插入顺序的集合
- LinkedHashSet:唯一支持记录插入顺序的 Set,通过链表维护顺序,哈希表保证唯一性。
- 对比其他结构:
- List(如 ArrayList):记录顺序但允许重复,不属于 Set。
- LinkedHashMap:记录键的插入顺序,但存储键值对而非单一元素。
选择建议
- 保留插入顺序:使用 LinkedHashSet(如按步骤记录唯一事件)。
- 自定义排序:使用 TreeSet(如按价格排序商品)。
- 性能考量:
- LinkedHashSet:查询/插入 O(1),内存开销略高于 HashSet。
- TreeSet:增删查 O(log n),适合大数据量排序场景。
高优先级
Java 实现 LRU
- 最简单的就是使用 LinkedHashMap 实现 LRU。
- LinkedList 实现基于 LRU 算法的缓存