八股(二)Java集合

目录

[😺Java 集合概览](#😺Java 集合概览)

[说说 List, Set, Queue, Map 四者的区别?](#说说 List, Set, Queue, Map 四者的区别?)

为什么要使用集合?

😺List

[Array(数组)vs ArrayList(动态数组)](#Array(数组)vs ArrayList(动态数组))

[ArrayList vs LinkedList](#ArrayList vs LinkedList)

[ArrayList 的扩容机制](#ArrayList 的扩容机制)

[集合中的 fail-fast 和 fail-safe 是什么?](#集合中的 fail-fast 和 fail-safe 是什么?)

😺Set

[Comparable 和 Comparator 的区别](#Comparable 和 Comparator 的区别)

无序性和不可重复性的含义是什么?

[​HashSet、LinkedHashSet、TreeSet 有什么区别?](#HashSet、LinkedHashSet、TreeSet 有什么区别?)

😺Queue

[Queue(单端队列)和 Deque(双端队列)有什么区别?](#Queue(单端队列)和 Deque(双端队列)有什么区别?)

PriorityQueue

BlockingQueue(阻塞队列)

[ArrayBlockingQueue vs LinkedBlockingQueue](#ArrayBlockingQueue vs LinkedBlockingQueue)

😺Map

[HashMap vs Hashtable](#HashMap vs Hashtable)

[HashMap vs TreeMap](#HashMap vs TreeMap)

[HashMap 的底层实现](#HashMap 的底层实现)

[HashMap 的长度为什么是 2 的幂次方](#HashMap 的长度为什么是 2 的幂次方)

[HashMap 多线程操作为什么会导致死循环](#HashMap 多线程操作为什么会导致死循环)

[HashMap 为什么线程不安全](#HashMap 为什么线程不安全)

[ConcurrentHashMap 和 Hashtable 的区别](#ConcurrentHashMap 和 Hashtable 的区别)

ConcurrentHashMap

[JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?](#JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?)

[ConcurrentHashMap 能保证复合操作的原子性吗?](#ConcurrentHashMap 能保证复合操作的原子性吗?)


😺Java 集合概览

Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 、Queue。

说说 List, Set, Queue, Map 四者的区别?

集合(Collection)是 Java 中专门用来存储多个数据的容器,一个"高级数组",比数组强很多,是**用来批量管理对象的数据结构。**相比数组,集合具有以下优势:

  • 长度可动态扩容
  • 提供丰富的增删改查操作
  • 支持排序、去重
  • 部分集合支持键值对存储

Java 集合你可以先分成两大体系:

1. Collection(单列集合)

  • List(有序、可重复、支持下标访问): ArrayList 动态数组LinkedList 双向链表 、Vector。

    List<Integer> list = new ArrayList<>():底层Object[],查询快、尾部插入快、中间插入慢。
    List<Integer> list = new LinkedList<>():底层双向链表,插入删除快、随机查询慢。
    Vector:现在基本不用,大量使用synchronized,所以线程安全,但性能较低。

  • Set(无序、不可重复): HashSet 、LinkedHashSet、TreeSet。
    HashSet:底层HashMap,所以去重本质依赖hashCode()、equals()。
    LinkedHashSet:底层HashSet + 双向链表,保持插入顺序。
    TreeSet:底层红黑树(自平衡二叉搜索树),自动排序、去重。

  • Queue(有序、可重复、先进先出): PriorityQueue、ArrayDeque。
    PriorityQueue:底层Object[],默认小顶堆,即最小值优先出队。
    ArrayDeque:底层动态循环数组,可以两头操作。

2. Map(双列集合 key-value)

  • Map(键值对,key唯一): HashMap 、LinkedHashMap、Hashtable、TreeMap
    HashMap:底层数组 + 链表 + 红黑树,拉链法解决哈希冲突,链表转红黑树,目的提高查询效率。
    LinkedHashMap:底层HashMap + 双向链表,保证插入顺序。
    Hashtable:现在基本不用,大量使用synchronized,所以线程安全,但性能较低,底层数组 + 链表,保证插入顺序。
    TreeMap:底层红黑树,key 自动排序。

    Collection
    ├── List
    ├── Set
    └── Queue

    Map
    ├── HashMap
    ├── LinkedHashMap
    └── TreeMap

Map<Integer,Integer>为什么是包装类?

Java 的泛型是通过类型擦除实现的编译后,泛型信息会被擦除,类型参数 Integer 会被替换为它的上界(如果没有显式指定,就是 Object)。因此 Map<Integer, Integer> 在运行时实际变成了 Map,其中存储的键和值都是 Object 类型。

基本数据类型(如 int、double)不是 Object 的子类,无法被当作对象处理,所以不能放在泛型里。必须使用对应的包装类(如 Integer、Double),因为它们继承自 Object。

虽然声明时必须写 Integer,但使用时可以直接放 int 值,编译器会自动装箱(int → Integer)和拆箱(Integer → int),这让我们写代码时几乎感觉不到差异。

  1. 语法限制:泛型参数必须是 Object 的子类,基本类型不是。
  2. 实现机制:类型擦除后泛型被替换为 Object,基本类型无法匹配。
  3. 使用便利:自动装箱/拆箱弥补了写法上的不便。

为什么要使用集合?

为什么不用数组?

  • 数组优点:查询快、结构简单、内存连续。
  • 数组缺点:长度固定、功能太少(存、取、遍历)

先看业务需求,再选数据结构。

是否需要 key-value?需要Map,不需要Collection。

Map→是否需要排序?不需要HashMap,需要按 key 排序 TreeMap(红黑树)。

Collection→是否需要去重?需要唯一Set,不需要唯一List。

Set→不需要排序 HashSet,需要排序 TreeSet。

List→默认首选 ArrayList,频繁头部 / 中间插入删除 LinkedList。

😺List

Array(数组)vs ArrayList(动态数组)

特性 Array(数组) ArrayList
长度 固定,创建时确定 动态,可扩容和缩容
类型 可以存基本类型或对象 只能存对象,基本类型需用包装类
泛型支持 不支持 支持泛型,类型安全
操作方式 只能按下标访问 提供丰富方法,如 add(), remove(), set()
灵活性 较低,需要手动处理插入/删除 高,自动处理元素移动、扩容等
创建时大小 必须指定大小 可指定初始容量,也可默认
  • ArrayList 使用灵活,适合绝大多数开发场景
  • Array 使用简单,性能略高,适合固定长度和存储基本类型场景

ArrayList vs LinkedList

ArrayList 尾增尾删 O(1)、头增头删 O(n)、中间增删 O(n)

LinkedList 头尾插删 O(1)、中间插删 O(n)、随机访问 O(n)

对比项 ArrayList LinkedList
底层结构 动态数组 双向链表
元素特点 有序、可重复、允许 null 有序、可重复、允许 null
查询访问 快(随机访问)O(1) 慢(需遍历查找)O(n)
增删效率 尾部快O(1) 头部和中间增删慢O(n),需移动元素 头尾增删快O(1),改指针即可 中间需要寻找O(n)
内存占用 较小 较大(每个节点存前后引用)
内存浪费 预留容量 单个元素更占内存
特有功能 普通列表操作 可做队列、双端队列、栈
常用方法 add/get/remove/set 与 ArrayList 一致 + offer/poll/peek/push/pop
适用场景 查询多、增删少 增删多、需队列 / 栈功能
开发首选 日常开发默认优先使用 特定队列 / 栈场景使用

**LinkedList 为什么不能实现 RandomAccess?**因为 LinkedList 底层是链表,内存地址不连续,只能通过指针逐个遍历访问元素,不支持像数组那样通过索引快速随机访问,因此不能实现 RandomAccess 接口。

复制代码
public interface RandomAccess {
}

看源码发现 RandomAccess 里面什么都没有,所以属于标记接口(不提供功能,只提供身份标识),用于标识实现类支持快速随机访问。

因为实现了 RandomAccess,所以变快了 ,ArrayList 并不是因为实现了它才变快,而是因为底层数组本身就支持快速随机访问,所以才实现该接口作为标识。先天就快,所以实现这个接口做标记,能力来自底层数据结构,不是来自接口。

在 binarySearch() 方法中,它要判断传入的 list 是否 RandomAccess 的实例,通过标记接口实现策略选择

  • 如果是,调用基于索引的二分查找 indexedBinarySearch() 方法。
  • 如果不是,那么调用迭代器方式遍历 iteratorBinarySearch() 方法。

ArrayList 的扩容机制

ArrayList 底层是动态数组,刚创建时容量 = 0,首次添加元素时扩容到 10,之后每次容量不足时按原容量的 1.5 倍扩容。

-- 什么时候第一次变成 10?为什么?

第一次执行 list.add 时。

calculateCapacity() 会 return Math.max(DEFAULT_CAPACITY, minCapacity);

DEFAULT_CAPACITY = 10,所以第一次扩成10。

-- 后续怎么扩容?

int newCapacity = oldCapacity + (oldCapacity >> 1); // 右移一位等价于除以 2

也就是 1.5 倍扩容。

-- 为什么不用其他扩容数量?空间和性能平衡。

扩太小,比如每次 +1:扩容次数太多,频繁复制数组,性能差,扩容是昂贵操作。

扩太大,比如每次 2 倍:浪费内存严重。

-- 扩容本质做了什么?

创建新数组 + 复制旧数组,时间复杂度O(n),所以扩容是昂贵操作。

-- ensureCapacity 的作用?

比如你明确知道要存 100 万条数据,就要一次性分配空间。list.ensureCapacity(1000000);

这样一次性分配空间,避免多次扩容、多次数组复制,性能更好。

这在批量导入、缓存预热里很常见。

ArrayList 默认不会自动缩容。如果每次删除都自动缩容,那就会频繁发生缩容、扩容,数组会不断复制,性能极差,所以 Java 设计上选择,宁可浪费一点空间,也不频繁复制数组。

不过可以采用手动缩容:java.util.ArrayList 的 trimToSize(),很少主动使用。

集合中的 fail-fast 和 fail-safe 是什么?

fail-fast:发现问题立刻报错

在遍历集合时,如果检测到集合被并发修改,立即抛出异常并终止运行。

典型代表:ArrayList、HashMap。

核心原理:modCount、expectedModCount。

集合每次结构修改时:modCount++。

迭代器在创建时记录:expectedModCount。

每次调用 next() 时都会校验:if (modCount != expectedModCount)。

若不一致,抛出:ConcurrentModificationException。

fail-safe:允许继续运行,安全失败

即使集合被修改,遍历过程仍可继续执行,不会抛异常。

典型代表:CopyOnWriteArrayList。

其底层采用:Copy-On-Write 写时复制。

即修改时复制出一个新数组,在新数组上完成修改,再替换原引用。

遍历时读取旧数组快照,因此不会受到并发修改影响。

核心区别:fail-fast 检查修改次数,fail-safe 使用快照副本。

实际开发怎么选:

普通单线程集合,这种场景根本没有线程安全问题,用 ArrayList、HashMap 性能最好。并发不能选,因为并发场景可能出现数据覆盖、数组越界、脏读、并发修改异常。

并发读多写少,用CopyOnWriteArrayList,它写操作时复制一份新数组,在新数组上修改,再替换原引用,读操作无锁,性能很高,特别适合高频读取场景。比如项目里店铺类型缓存列表、热门笔记标签 很适合,这些数据查询频率非常高,但更新很少,非常适合使用 CopyOnWriteArrayList。

CopyOnWriteArrayList 为什么线程安全?

CopyOnWriteArrayList 通过"写时复制(Copy-On-Write)"机制实现线程安全:读操作无锁直接访问旧数组,写操作加锁并复制新数组修改,最后替换引用,从而保证读写互不干扰。它的缺点是写操作成本较高,需要复制整个数组,因此适用于读多写少的场景。

😺Set

Comparable 和 Comparator 的区别

Comparable 位于 java.lang 包,核心方法是compareTo(T o) ,特点是由类自身实现排序规则,适用于对象的默认自然排序且一种排序规则

Comparator 位于 java.util 包,核心方法是compare(T o1, T o2) ,特点是由外部定义排序规则,适用于临时排序或多种排序规则场景,例如同一个 Person 类,今天可以按年龄排序,明天可以按姓名排序。

java 复制代码
class Person implements Comparable<Person> {
    private String name;
    private int studentId;

    @Override
    public int compareTo(Person o) {
        // 重写 Comparable 接口中的 compareTo 方法后,
        // Person 对象就具备了"默认排序规则"
        // 返回负数:当前对象排前面
        // 返回 0:认为两个对象相等
        // 返回正数:当前对象排后面
        return this.studentId - o.studentId;
    }
}

// 之后就可以直接使用默认排序规则
List<Person> list = new ArrayList<>();
list.add(new Person("Tom", 1002));
list.add(new Person("Jerry", 1001));

// 1)Collections.sort(list)
Collections.sort(list); // 会自动调用 compareTo,按 studentId 升序排序

// 2)List 自带 sort 方法(Java 8 常用)
list.sort(null); // 传 null 表示使用对象默认排序规则

// 3)stream 排序时也可以直接使用自然排序
list.stream().sorted().forEach(System.out::println);

// 4)Arrays.sort 也可以用于对象数组默认排序
Person[] arr = {
    new Person("Tom", 1002),
    new Person("Jerry", 1001)
};
Arrays.sort(arr); // 同样调用 compareTo

// 也可以作为 TreeSet / TreeMap 的排序依据
Set<Person> set = new TreeSet<>();
set.add(new Person("Tom", 1002));
set.add(new Person("Jerry", 1001));
java 复制代码
List<Person> list = new ArrayList<>(); 
list.sort(new Comparator<Person>() { 
    @Override
    public int compare(Person o1, Person o2) { 
        return o1.getAge() - o2.getAge(); 
    } 
}); 
// lambda 表达式版本 
list.sort((o1, o2) -> o1.getAge() - o2.getAge());

Comparable:内部默认排序。Comparator:外部自定义排序。

如果一个类只有一种固定排序方式,通常使用 Comparable。

如果需要多种排序规则,例如按年龄、姓名、成绩排序,通常使用 Comparator。

无序性和不可重复性的含义是什么?

  1. 无序性

无序性并不等于随机性,它的真正含义是元素在底层存储时,并不是按照插入顺序或数组索引顺序存放,而是由元素的哈希值决定存储位置。

例如 HashSet 底层基于 HashMap 实现,元素的位置由hashCode()计算得到,因此遍历时不保证顺序。

  1. 不可重复性

不可重复性是指集合中不能存在逻辑相等的元素 。判断是否重复时,通常先比较hashCode(),若哈希值相同,再调用equals(),若返回 true,则认为元素重复,不会再次加入集合。

注意:为了正确保证不可重复性,自定义对象通常需要同时重写equals()、hashCode(),否则可能导致重复元素判断失效。

​HashSet、LinkedHashSet、TreeSet 有什么区别?

最大的区别:怎么存储?

集合 底层 顺序 时间复杂度
HashSet HashMap 无序 O(1)
LinkedHashSet LinkedHashMap 插入顺序 O(1)
TreeSet 红黑树 自动排序 O(log n)

😺Queue

Queue(单端队列)和 Deque(双端队列)有什么区别?

Queue 是单端队列,通常一端入队、一端出队,遵循 FIFO,常用方法:offer() poll() peek()。

Deque 是双端队列,头尾两端都可以进行插入和删除操作,同时可以模拟队列和栈,常用方法offerFirst() offerLast() pollFirst() pollLast() peekFirst() peekLast() 。

PriorityQueue

它与普通 Queue 的主要区别在于元素出队顺序与优先级相关,而不是插入顺序,可以理解为Vip队列,优先级最高的元素总是最先出队。

底层实现:二叉堆(Heap),堆的底层是可变长数组,默认实现为小顶堆

为什么底层不用链表?

因为堆适合用数组按下标快速定位父子节点,链表无法高效随机访问。

时间复杂度:插入O(log n)、删除堆顶O(log n) 、获取堆顶元素O(1)

为什么是 O(logn)?因为底层是堆结构,插入时需要上浮,最多移动树高层数,即 logn。

可通过 Comparator 自定义优先级规则,比如怎么改成大顶堆?

PriorityQueue<Integer> pq = new PriorityQueue<>((a,b)->b-a);

典型应用:堆排序、Top K 问题

BlockingQueue(阻塞队列)

其核心特点是支持线程在特定条件下阻塞等待,主要体现在两个方面:

  1. 队列为空时阻塞
    当消费者线程执行取元素操作(如 take())时,如果队列为空,线程会进入阻塞状态,直到队列中有新的元素可取。
  2. 队列已满时阻塞
    当生产者线程执行放元素操作(如 put())时,如果队列已满,线程会进入阻塞状态,直到队列中有空位。

因此,BlockingQueue 非常适合实现生产者-消费者模型。

在该模型中:

  • 生产者线程负责向队列中放入数据
  • 消费者线程负责从队列中取出数据并处理

常用方法:

放元素:put():满了会阻塞。offer():不阻塞,失败返回 false。

取元素:take():空了会阻塞。poll():不阻塞,空返回 null。

为什么线程池喜欢用 BlockingQueue?

因为任务提交和工作线程消费任务天然符合生产者-消费者模型,可以实现线程间安全通信和任务缓冲。

ArrayBlockingQueue vs LinkedBlockingQueue

ArrayBlockingQueue 和 LinkedBlockingQueue 都是 Java 并发包中常用的阻塞队列实现,并且都是线程安全的。

底层实现不同:ArrayBlockingQueue 基于数组实现,LinkedBlockingQueue 基于链表实现。

是否有界:ArrayBlockingQueue 是有界队列,创建时必须指定容量,new ArrayBlockingQueue<>(10)。LinkedBlockingQueue 默认可以看作无界队列。

锁机制不同:ArrayBlockingQueue 使用的是同一把锁,因此生产和消费不能真正并行执行。LinkedBlockingQueue 使用的是两把锁分离机制,即生产者和消费者可以并发执行,减少锁竞争,提高吞吐量。

Q:为什么 LinkedBlockingQueue 性能通常更好?

A:因为它采用 putLock 和 takeLock 两把锁,生产和消费可以并行执行,减少锁竞争。

Q:为什么线程池常用 LinkedBlockingQueue?

A:因为其支持较高并发吞吐量,且默认容量很大,适合作为任务缓冲队列。

😺Map

HashMap vs Hashtable

HashMap 和 Hashtable 都是用于存储键值对(key-value)的哈希表实现,但它们有以下几个核心区别:

线程安全性不同:HashMap 是 线程不安全, Hashtable 是 **线程安全,**原因是 Hashtable 的大部分方法都使用了 synchronized 修饰。

性能不同:由于 Hashtable 使用了 synchronized 加锁机制,因此性能通常低于 HashMap,所以实际开发中 HashMap 使用远多于 Hashtable。

底层数据结构不同:HashMap 使用数组 + 链表 + 红黑树,当链表长度超过 8 且数组长度大于等于 64 时,会转为红黑树,目的是降低查找时间复杂度,即 O(n) -> O(logn)。Hashtable 没有红黑树优化机制,仍主要使用数组 + 链表。

哈希算法不同:HashMap 会对 hashCode 进行扰动处理,h ^ (h >>> 16),目的是让高位参与运算,减少哈希冲突。Hashtable 基本直接使用 key 的 hashCode。因此 HashMap 哈希分布通常更均匀。

HashMap vs TreeMap

**底层数据结构不同:**HashMap 基于哈希表(数组 + 链表 + 红黑树),TreeMap 基于红黑树。

**是否有序:**HashMap 无序,TreeMap 默认按 key 升序排序,也可以自定义 Comparator。

**时间复杂度:**HashMap 查找、插入平均 O(1),TreeMap 查找、插入 O(log n)。

**功能差异:**TreeMap 支持范围查询(subMap、headMap、tailMap),TreeMap 支持导航操作(ceiling、floor、higher、lower)。

适用场景:HashMap 高效查找,无需排序,TreeMap 需要排序或范围查询。

Q1:TreeMap 为什么是有序的?

A:因为底层使用红黑树结构,每次插入 key 时都会按照比较规则(Comparable 或 Comparator)进行排序维护。

Q2:HashMap 为什么能做到 O(1) 查找?

A:通过 hash 函数计算 key 的位置,直接定位数组索引,减少遍历。

Q3:TreeMap 和 LinkedHashMap 有什么区别?

A:TreeMap 按 key 排序(红黑树),LinkedHashMap 按插入顺序或访问顺序(双向链表 + 哈希表)

HashMap 的底层实现

HashMap 本质是一个数组,每个位置是一个桶(bucket),每个桶中可能存:1个节点、链表、红黑树。

拉链法解决哈希冲突:hash 冲突时,用链表挂在同一个桶下面。

JDK8 关键升级(红黑树):当链表过长时,默认链表长度 > 8 并且数组长度 ≥ 64,链表会转成红黑树,链表查找O(n),红黑树查找O(log n)。

为什么不是直接转红黑树?①先扩容更划算,扩容可以减少 hash 冲突,数据重新分布,比维护红黑树成本低。②红黑树维护成本高,结构复杂,不适合小规模数据。经验阈值(8 + 64)。

HashMap 的长度为什么是 2 的幂次方

第一,提高运算效率

当数组长度是 2 的幂次方时,就可以把取余运算转化成位运算,位运算效率高于取模运算。

复制代码
hash % length = hash & (length - 1)

第二,保证元素分布均匀。扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。

第三,提高扩容效率。扩容后只需检查哈希值高位的变化来决定元素的新位置。

Q:HashMap 默认长度是多少?

A:默认长度是 16。

HashMap 多线程操作为什么会导致死循环

根本原因是 HashMap 本身线程不安全,并且在扩容迁移链表节点时采用了头插法。当多个线程同时对同一个桶中的链表进行扩容迁移时,可能同时修改节点的 next 指针,导致链表反转过程中形成环形链表。一旦形成环形链表,后续执行 get() 查询时会沿着 next 指针无限遍历,最终导致程序陷入死循环。

在 JDK1.8 中,HashMap 将扩容迁移方式改为尾插法,避免了链表反转,从而解决了死循环问题。

但需要注意,JDK1.8 的 HashMap 依然不是线程安全的,在并发环境下仍可能出现数据覆盖和数据丢失问题,因此不建议在多线程场景下使用。

并发环境推荐使用:ConcurrentHashMap。

HashMap 为什么线程不安全

HashMap 不是线程安全的,在多线程环境下进行并发 put / remove 操作时,可能导致以下问题:

  1. 数据丢失:并发 put 操作可能导致一个线程的写入被另一个线程覆盖。
  2. 数据覆盖(丢失写入):HashMap 发生 hash 冲突时,多个线程可能同时操作同一个桶位置。由于 put 操作不是原子性的(包含:计算 hash、判断桶位置、插入节点等多个步骤),线程切换可能导致后一个线程覆盖前一个线程写入的数据,从而产生数据丢失。
  3. size 统计不准确:HashMap 的 size++ 操作不是原子操作,在多线程环境下可能发生"丢失更新"。多个线程同时执行 ++size,会导致实际插入多个元素,但 size 增长次数小于实际插入次数。
  4. 无限循环:在 JDK 7 及以前的版本中,并发扩容时,由于头插法可能导致链表形成环,从而在 get 操作时引发无限循环,CPU 飙升至 100%。

JDK1.8 虽然优化了扩容方式(尾插法),避免了死循环问题,但仍然无法保证线程安全。

Q:HashMap 为什么会发生数据覆盖?

A:因为 put 操作不是原子性的,多线程同时操作同一个桶时可能互相覆盖节点。

Q:size 为什么会不准确?

A:因为 ++size 不是原子操作,多个线程同时执行会发生丢失更新。

Q:JDK1.8 是否解决了所有线程问题?

A:没有,只解决了 JDK1.7 的死循环问题,但仍存在数据竞争问题。

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 都是线程安全的 Map 实现,但它们在实现方式和性能上有本质区别。

  1. 底层数据结构不同
    Hashtable:数组 + 链表(无红黑树优化)。ConcurrentHashMap:JDK1.7 为 Segment 分段数组 + 链表;JDK1.8 为 Node 数组 + 链表 / 红黑树
  2. 线程安全实现方式不同(核心重点)
    Hashtable:使用一把全局锁(synchronized 修饰方法),所有操作都竞争同一把锁,并发度极低
    ConcurrentHashMap:JDK1.7:分段锁(Segment),不同段可以并发访问。JDK1.8:CAS + synchronized(锁粒度缩小到桶级别)。
  3. 性能差异
    Hashtable:同一时刻只允许一个线程操作,吞吐量低。ConcurrentHashMap:支持更高并发,性能远高于 Hashtable。
  4. 扩容与结构演进(JDK1.8)
    Hashtable = 一把锁(粗粒度锁)
    ConcurrentHashMap = 分段/桶级别锁(细粒度锁)+ CAS

Q:Hashtable 为什么性能差?

A:因为所有操作都使用同一把锁,导致多线程串行执行。

Q:ConcurrentHashMap JDK1.8 为什么取消 Segment?

A:Segment 粒度较粗,难以提升并发度,改为 CAS + synchronized 提高性能。

ConcurrentHashMap

ConcurrentHashMap 在 JDK1.7 和 JDK1.8 中采用了不同的方式来实现线程安全。

JDK1.7 中,ConcurrentHashMap 采用 Segment 分段锁机制:整个 Map 被分成多个 Segment,每个 Segment 继承 ReentrantLock,相当于一把独立的锁。多个线程可以同时访问不同 Segment,从而实现高并发访问。每个 Segment 内部结构类似 HashMap(数组 + 链表),锁粒度是 Segment 级别。

JDK1.8 中,ConcurrentHashMap 取消了 Segment 结构,改为 Node 数组 + 链表 / 红黑树 + CAS + synchronized 实现线程安全。其核心思想是:

  • 通过 CAS 保证无冲突情况下的原子操作(如初始化、插入空桶)
  • 当发生冲突时,对桶的首节点加 synchronized 锁(桶级别锁)
  • 锁粒度进一步缩小,提高并发性能

同时,当链表长度超过阈值(默认 8)时,会转为红黑树,以提升查询效率(O(n) → O(log n))。

Q:JDK1.8 为什么要取消 Segment?

A:因为 Segment 粒度较大,限制并发度,改为桶级锁 + CAS 可以进一步提升并发性能。

Q:synchronized 锁的是整个 Map 吗?

A:不是,只锁当前桶的首节点,锁粒度非常细。

JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

线程安全实现方式不同

JDK1.7:采用 Segment 分段锁机制(把一个大 Map 拆成多个小 Map),每个 Segment 继承 ReentrantLock(每个 Segment 本身就是一把锁),实现分段加锁,提高并发度。

JDK1.8:取消 Segment,改为 Node 数组 + CAS + synchronized(桶级锁)实现线程安全。

底层数据结构不同

JDK1.7:Segment + HashEntry(数组 + 链表)。

JDK1.8:Node 数组 + 链表 + 红黑树(和 HashMap 基本一样,链表过长会树化)。

锁粒度不同

JDK1.7:锁粒度是 Segment 级别,粒度还是偏大。

JDK1.8:锁粒度是桶(Node)级别,只锁链表或红黑树头节点。

并发能力不同

JDK1.7:并发度受 Segment 数量限制(默认16个Segment,所以也默认支持16线程并发)。

JDK1.8:并发粒度更细,因为锁的是桶,桶数量远远大于16,并发能力更强。

ConcurrentHashMap 能保证复合操作的原子性吗?

ConcurrentHashMap 线程安全 ≠ 复合操作安全

复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。

如何保证复合操作原子性?ConcurrentHashMap 提供了专门方法:putIfAbsent、computeIfAbsent

Q1:为什么 containsKey + put 不安全?

A:因为两个操作之间可能被线程切换打断。

Q2:computeIfAbsent 为什么安全?

A:因为内部是原子操作(CAS + 同步控制),不会被插入打断。

Q3:能不能用 synchronized 保证?

A:可以,但会退化为粗粒度锁,性能下降,不推荐。

相关推荐
星乐a2 小时前
String 不可变性与常量池深度解析
java·开发语言
captain3762 小时前
ACM模式下Java输入输出函数为什么会超时?及解决方法
java·开发语言
程序员老邢2 小时前
【产品底稿 04】商助慧 V1.1 里程碑:爬虫入库 + MySQL + Milvus 全链路打通
java·爬虫·mysql·ai·springboot·milvus
2601_950703942 小时前
Java安全编程与静态分析实战
java
唐叔在学习2 小时前
Python移动端应用消息提醒开发实践
开发语言·python
好家伙VCC2 小时前
**发散创新:基于Python与OpenCV的视频流帧级分析实战**在当前人工智能与计算机视觉飞速发展的背景下
java·人工智能·python·计算机视觉
SimonKing2 小时前
大V说’AI替代不了你’,但现实是——用AI的人正在替代你
java·后端·程序员
暴力求解2 小时前
C++ ---string类(三)
开发语言·c++
Pocker_Spades_A2 小时前
Python快速入门专业版(五十七)——POST请求与模拟登录:从表单分析到实战(以测试网站为例)
开发语言·python