重点
核心实现类对比
关键问题
- 线程安全改造 - Collections.synchronizedList():包装普通List为线程安全,需手动同步复合操作
- CopyOnWriteArrayList:写时复制新数组,读操作无锁,适合读多写少场景
 
- 遍历与修改 - 普通for循环:允许修改元素(通过索引),但需手动维护索引 
- 增强for循环:禁止结构性修改(add/remove),否则抛 - ConcurrentModificationException
- 迭代器:通过 - iterator.remove()安全删除元素,支持双向遍历(ListIterator)
 
- 删除操作性能 - ArrayList.remove(int index):O(n)时间复杂度(需移动元素)
- LinkedList.removeFirst():O(1)时间复杂度(头尾操作优化)
 
实现 & 差异
- ArrayList:动态数组的扛把子 - 底层结构:基于动态数组实现,初始容量 10,每次扩容 1.5 倍 
- 性能特性: - 随机访问快(O(1)),但中间插入/删除需要移动元素(O(n)) 
- 适合读多写少场景,例如缓存、配置项存储 
 
- 线程安全:非线程安全,多线程环境下需手动同步 
 
- LinkedList:链表的灵活艺术家 - 底层结构:基于双向链表实现,无容量限制 
- 性能特性: - 头尾插入/删除快(O(1)),但随机访问需遍历(O(n)) 
- 额外实现 Deque 接口,可作队列/栈使用 
 
- 适用场景:高频增删操作(如消息队列)、实现栈/双向队列 
 
- Vector:过时的安全卫士 - 底层结构:类似 ArrayList,但所有方法通过 synchronized 实现线程安全 
- 缺点: - 性能差(同步开销大),扩容策略更激进(默认翻倍) 
- 现代开发中已被 CopyOnWriteArrayList 或 Collections.synchronizedList() 取代 
 
 
- CopyOnWriteArrayList:并发的读多写少专家 - 底层机制:写时复制(COW),写操作加锁复制新数组,读操作无锁。 
- 优点:读性能极高(无锁并发),适合读多写少的高并发场景(如配置中心) 
- 缺点:写操作性能差(需复制整个数组),内存占用高 
 
- 关键对比与选型建议 
冷知识:
LinkedList的get(index)方法会触发「链表遍历杀」——越靠近中间越慢
避坑指南:
🚫 用
ArrayList遍历删除元素需用迭代器,否则触发ConcurrentModificationException
✅ 超大数据量优先测试不同实现的耗时(如
LinkedList头插快,但中间插入可能比ArrayList慢)
遍历修改
方式与规则
- 普通 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 通过复制新数组实现无锁读,但写操作性能较低(适合读多写少场景) 
删除元素
快速删除指定下标元素
- ArrayList 删除指定下标元素 - 方法:直接调用 remove(int index)。 
- 时间复杂度:O(n)(需移动后续元素)。 
- 优化建议:高频删除中间元素可改用 LinkedList,或反向遍历避免索引错位。 
 
- LinkedList 删除指定下标元素 - 方法:调用 remove(int index)。 
- 时间复杂度:O(n)(需遍历链表)。 
- 头尾优化:用 removeFirst()/removeLast() 可将时间降至 O(1)。 
 
- 线程安全场景 - CopyOnWriteArrayList:调用 remove(int index) 会创建新数组,适合读多写少,但写性能差(O(n))。 
 
- 遍历时删除 - 安全方法:使用迭代器的 Iterator.remove(),避免 ConcurrentModificationException。 
 
- 性能对比与选型 
- 面试核心点 - ArrayList 和 LinkedList 按索引删除的时间复杂度均为 O(n),但 LinkedList 头尾删除可优化至 O(1) 
- 高频删除优先选 LinkedList 或反向遍历 ArrayList 
- 线程安全场景权衡写性能,选 CopyOnWriteArrayList 或 ConcurrentLinkedDeque 
 
线程安全
为什么 ArrayList 不是线程安全的,具体来说是哪里不安全?
ArrayList 线程安全吗?为什么?把 ArrayList 变成线程安全有哪些方法?
非线程安全
- 非线程安全:默认设计不支持多线程并发操作 
- 并发风险: - 数据不一致:多线程同时修改导致元素丢失或重复 
- 数组越界:动态扩容时并发操作可能触发非法索引访问 
- ConcurrentModificationException:迭代过程中发生结构性修改 
 
不安全的点
- 非原子操作引发数据竞争 - size 递增非原子性:size++ 操作实际由读取旧值、计算新值、写入新值组成,多线程同时执行可能导致最终结果小于实际增加次数 
- 元素存储覆盖:多个线程在相同索引位置执行 elementData[i] = e,后写入的线程会覆盖前者的数据 
 
- 动态扩容竞态条件 - 重复扩容数据丢失:多个线程同时检测到容量不足,各自创建更大的新数组并复制数据,最终仅保留最后一个线程的扩容结果 
- 扩容间隙越界风险:线程 A 扩容过程中,线程 B 仍使用旧容量索引插入数据,触发数组越界异常 
 
- 内存可见性问题 - 脏读现象:线程修改数组元素后未及时同步到主内存,其他线程可能读取到修改前的旧数据 
- 失效状态传播延迟:扩容后的新数组引用更新未使用同步机制,导致部分线程继续访问旧数组 
 
- 迭代器快速失败机制缺陷 - 并发修改检测失效:迭代器的 expectedModCount 与集合的 modCount 在多线程环境下无法保持同步 
- 结构变更暴露风险:迭代过程中其他线程删除元素,可能导致当前迭代器访问到已被移除的索引位置 
 
- 复合操作非原子性 - 检查-执行间隙干扰:如 if(!contains(e)) { add(e) } 操作中,条件判断与添加动作之间可能被其他线程插入新操作 
- 中间状态不一致:扩容过程中新旧数组交替阶段,其他线程可能读取到仅部分数据迁移完成的中间数组 
 
- 指令重排序干扰 - 初始化顺序错乱:对象初始化过程中,JVM 优化可能打乱 elementData 赋值与 size 初始化的顺序 
- 写操作乱序执行:元素赋值与 size 更新的顺序被重排,导致其他线程看到 size 变化但对应位置数据未更新 
 
线程安全改造
- Collections.synchronizedList - 实现方式:通过装饰器模式对所有方法添加对象级锁 
- 特点 - 读写操作均加锁,适用于中等并发场景 
- 迭代器遍历需显式同步 
- 组合操作需外部同步控制 
 
 
- CopyOnWriteArrayList - 核心机制:写操作复制新数组,读操作访问旧数组 
- 优势 - 无锁读操作,适合读多写少场景 
- 数据最终一致性保证 
 
- 缺陷 - 内存占用随写操作次数线性增长 
- 不适用实时性要求高的场景 
 
 
- 手动同步锁 - 实现要点 - 自定义锁对象控制关键代码段 
- 缩小同步代码块范围提升性能 
 
- 适用场景: - 需要精细化控制锁粒度 
- 高并发环境下的特定操作优化 
 
 
- Vector(遗留方案) - 特点 - 方法级同步锁导致性能瓶颈 
- 扩容策略与 ArrayList 不同(默认 2 倍 vs 1.5 倍) 
 
 
- 使用建议:仅用于兼容旧系统,新项目避免使用 
方案对比与选型
- 性能维度 - 高频读:CopyOnWriteArrayList > 手动锁 > synchronizedList > Vector 
- 高频写:手动锁 > synchronizedList > Vector > CopyOnWriteArrayList 
 
- 内存维度:CopyOnWriteArrayList 内存消耗最大 
- 开发成本:synchronizedList 实现成本最低 
底层原理
- JVM 内存模型 - 未同步操作可能违反 happens-before 原则 
- volatile 关键字保证可见性但无法解决原子性问题 
 
- 锁优化技术 - 偏向锁 → 轻量级锁 → 重量级锁的升级过程 
- 自旋锁减少线程切换开销 
 
- 硬件支持 - CAS 操作依赖 CPU 的 cmpxchg 指令 
- 内存屏障保证指令执行顺序 
 
扩容机制
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
- 高频随机访问需求 - 基于动态数组结构,通过索引访问元素时间复杂度为 O(1) 
- 典型场景:电商商品信息存储、数据分析中的快速遍历统计 
 
- 尾部操作密集型任务 - 尾部插入/删除时间复杂度为 O(1) 
- 典型场景:日志追加存储、传感器数据顺序采集 
 
- 内存敏感型场景 - 连续内存空间存储,无额外指针开销 
- 典型场景:百万级配置参数存储、移动端内存限制场景 
 
LinkedList
- 频繁中间/头部操作 - 插入/删除时间复杂度为 O(1) 
- 典型场景:聊天消息队列、文本编辑器撤销链 
 
- 特殊数据结构实现 - 原生支持双端队列操作 
- 典型场景:任务调度插队系统、LRU 缓存算法 
 
- 动态伸缩需求场景 - 无需预分配内存,按需动态增长 
- 典型场景:网络数据包接收、游戏事件处理器链 
 
选型决策
- 选择 ArrayList 的明确条件 - 主要操作为索引访问或尾部增删 
- 数据规模可预估且波动范围较小 
- 需要利用 CPU 缓存空间局部性原理 
 
- 选择 LinkedList 的明确条件 - 主要操作为非尾部位置增删 
- 需要实现队列/栈等数据结构 
- 内存资源充足但需规避扩容开销 
 
性能优化
- ArrayList 使用禁忌 - 避免在循环中频繁执行 contains() + add() 组合操作 
- 初始化时通过 ensureCapacity() 预分配空间 
 
- LinkedList 使用禁忌 - 避免通过索引遍历(时间复杂度 O(n^2)) 
- 警惕超大数据量下的内存占用激增问题 
 
组合使用
- 冷热数据分离存储 - 高频修改数据使用 LinkedList,只读数据使用 ArrayList 
 
- 分段混合存储 - 将大数据集拆分为多个 ArrayList 子块,用链表维护块关系 
 
- 动态切换机制 - 根据运行时监控数据自动切换 List 实现类型 
 
List & CopyonWriteArraylist
线程安全的 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:需线程安全且性能要求不高的多线程场景。实际开发中更推荐通过并发包实现线程安全 
 
             
           
             
                         
             
            
评论区