Skip to content

右侧栏

JAVA 容器集合面试题

常见的集合有哪些?

java 集合框架主要集中在 java.util包中,可以分为以下几大类:

Collection 接口体系(单个元素集合)

  1. List(有插入顺序,可以重复)
    • ArrayList:基于动态数组实现,查询快,增删慢(特别是中间插入/删除)
    • LinkedList:基于双向链表,适合频繁插入/删除的场景
    • Vector:线程安全的(已过时),功能与 ArrayList 类似
  2. Set(无序,不重复)
    • HashSet:基于 HashMap 实现,元素无序,允许为 Null。
    • LinkedHashSet:有插入顺序,基于 HashSet+链表实现。
    • TreeSet:有序(按照元素自然排序或比较器),基于红黑树
  3. Queue(队列/双端队列)
    • PriorityQueue:优先队列,元素按照优先级排列
    • ArrayDeque:双端队列,比 Stack 更推荐
    • LinkedList:也实现了 Queue 接口

Map 接口体系(键值对集合)

  1. HashMap(最常用)
    • 无序,允许一个 Null 或多个 null 值(Key 只能为一个,Value 可以有多个null),线程不安全
  2. LinkedHashMap
    • 有插入顺序,常用于需要顺序便利 Map
  3. TreeMap
    • 按键排序(自然排序或自定义比较器排序),基于红黑树
  4. HashTable
    • 线程安全的,老旧,不建议使用
  5. ConcurrentHashMap
    • 支持高并发,线程安全的 HashMap 替代方法。

常见的并发集合有哪些

并发 Map (键值对结构)

  1. ConcurrentHashMap

    ConcurrentHashMap 是 HashMap 的线程安全版本。

    • 替代 HashTable,线程安全
    • JDK8 以前使用 Segment 分段锁
    • JDK8 后使用 CAS+synchronized+Node 数组+红黑树;
    • 适合高并发读写
  2. ConcurrentSkipListMap

    • 基于跳表(SkipList)实现的有序 Map,支持按顺序遍历。
    • 是线程安全的版本的 TreeMap。

并发 List(列表结构)

  1. CopyOnWriteArrayList
    • 写时复制。它的实现原理就是在添加元素的时候对方法加锁,先把 List 列表复制一份,再添加新的元素。这样就实现了线程安全性。读多写少场景下非常适用;
    • 每次修改都会复制整个数组,开销大,但读操作无锁超快。

并发 Set(集合结构)

  1. CopyOnWriteArraySet
    • 基于 CopyOnWriteArrayList 实现
    • 支持线程安全,适合读多写少
    • 自动去重
  2. ConcurrentSkipListSet
    • 基于跳表,线程安全,元素有序;
    • 可视为 ConcurrentSkipListMap 的 key 集合版本

并发 Queue(队列)

  1. ConcurrentLinkedQueue
    • 非阻塞队列,是一种基于单向链表结构的无界并发队列,基于 CAS+链表;
    • 适合高并发场景中的生产者消费者模型
  2. LinkedBlockingQueue
    • 阻塞队列(支持容量限制)
    • 基于链表,读写分别加锁,性能优
  3. ArrayBlockingQueue
    • 有界阻塞队列,基于数组实现
    • 每次读写都加锁,适合固定长度的任务队列。
  4. PriorityBlokingQueue
    • 带优先级的阻塞队列
    • 元素会根据优先级排序,不保证 FIFO。
  5. DelayQueue
    • 支持延迟获取元素的阻塞队列;
    • 元素必须实现 Delayed 接口。

总结表格

类型并发集合类特点
MapConcurrentHashMap无锁/部分锁,最常用
MapCouncurrentSkipListMap有序 Map,跳表实现
ListCopyOnWriteArrayList写时复制,适合读多写少
SetCopyOnWriteArraySetSet 版本写时复制
SetConcurrentSkipListSet有序,跳表实现
QueueConcurrentLinkedQueue非阻塞,无界
QueueLinkedBlockingQueue阻塞,有界
QueueArrayBlockingQueue阻塞,定长数组
QueueDelayQueue支持延时任务
QueuePriorityBlockingQueue元素有优先级

Comparable 和 Comparator 接口的区别

  • Comparable 是排序接口,若类实现了 Comparable 接口,并实现起 compareTo 排序方法,就表示该类支持排序,相当一个内部排序器。需要对类本身进行修改
  • Comparator 是比较器接口,可以新建多个 Comparator 接口的实现来实现自定义排序,相当于一个外部排序器,不需要对类做修改

示例代码对比

  • Comparable(类内部定义排序逻辑)

    java
    public class Student implements Comparable<Student> {
        private String name;
        private int age;
    
        // 构造函数、getter、setter等略
    
        @Override
        public int compareTo(Student other) {
            return this.age - other.age; // 按年龄升序排序
        }
    }

    使用:

    java
    List<Student> list = new ArrayList<>();
    Collections.sort(list); // 使用 compareTo 排序
  • Comparator(外部定义排序逻辑)

    java
    public class Student {
        private String name;
        private int age;
    
        // 构造函数、getter、setter等略
    }

    定义比较器:

    java
    public class AgeComparator implements Comparator<Student> {
        @Override
        public int compare(Student s1, Student s2) {
            return s1.getAge() - s2.getAge();
        }
    }

    使用:

    java
    List<Student> list = new ArrayList<>();
    Collections.sort(list, new AgeComparator()); // 使用自定义比较器排序

总结对比

特性ComparableComparator
java.langjava.util
方法名compareTo(T o)compare(T o1, T o2)
是否改变类
是否支持多种排序否 (只能一个 ComparaTo)是(多个 Comparator 实现)
使用场景默认排序多样化排序,自定义灵活

集合使用泛型的优点

  1. 类型安全,使用泛型之后,集合中只能存放特定类型的对象,避免了类型转换错误。
  2. 避免强制类型转换,泛型让你在取值的时候不需要手动强制转换,代码更加整洁。提高代码可读性和可维护性
  3. 优化 JVM 运行时环境,因为不会有类型检查的字节码指令

总结一句话,泛型=编译时类型检查+更少的强制转换+更清晰的代码结构

为什么 Map接口不继承 Collection 接口?

简单说:它们的设计目的不同,结构不相同,如果硬继承反而显得不合理。

  • Collection 关注的是元素的集合,例如:一个 List<String> 就是一堆字符串
  • Map 是键值对集合,每一项都是 Key->Value,和 Collection不是用一回事
  • 两者方法的语义也不相同,Collection 的很多方法对 Map 来说都不适用。

常见的线程安全的 Map 有哪些?

  1. ConcurrentHashMap

    • Java 8 起采用 CAS+synchronized 替代分段锁(java7)

    • 支持高并发读写,性能优秀

    • 不允许 Null 键或 Null 值。

    • java

    Map<String, String> map = new ConcurrentHashMap<>();

    
     Map<String, String> map = new ConcurrentHashMap<>();
  2. CocurrentSkipListMap

    • 支持按 Key 排序,类似 TreeMap,但是线程安全

    • 内部接口是跳表(Skip List)适合范围查询、排序等

    • java

    Map<Integer, String> map = new ConcurrentSkipListMap<>();

  3. Collections.synchronizedMap(Map<K,V> map)

    • 给普通 Map 如 HashMap 加一层同步锁

    • 线程安全但是性能较差,并发量高时会成为瓶颈

    • java

    Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

  4. HashTable

    • 所有方法都使用的 Synchronized,性能较差。
    • 属于早期 java 设计,不建议新项目使用。

总结

名称所在包特点是否推荐
Hashtablejava.util古老的同步 Map,所有方法都加了 synchronized❌ 不推荐,效率低
Collections.synchronizedMap()java.util.Collections对普通 Map 加了一层同步包装⚠️ 可用,但不如 ConcurrentMap
ConcurrentHashMapjava.util.concurrent高效并发 Map,分段锁或 CAS 实现✅ 推荐使用
ConcurrentSkipListMapjava.util.concurrent支持并发的有序 Map(类似 TreeMap✅ 用于需要排序的并发场景
ImmutableMap(Guava)com.google.common.collect不可变线程安全 Map,构造后只读✅ 用于读多写少场景

适用建议

场景推荐
高并发读写ConcurrentHashMap
有序并发 MapConcurrentSkipListMap
读多写少且初始化后不变ImmutableMap(Guava)
简单同步封装Collections.synchronizedMap()(低并发或临时使用)
老代码兼容Hashtable(尽量替换)

HashMap 在 JDK8 中有哪些改变?

  1. 引入红黑树优化链表结构,这是最核心的变化

    • 原来 JDK7中,hash 冲突的 Key 都放在桶的链表中(拉链法)
    • 冲突多时,链表长度较长,查询效率从 O(1),退化为 O(N)
    • JDK8中,如果桶中的链表长度超过阈值(默认是 8),就将链表转换成红黑树
    • 红黑树的查询性能是 O(log n),大大提高了在 hash 冲突下的性能
  2. 延迟树化机制,转换成红黑树不是立即执行

    • 只有桶中的元素数量≥ 8 且整个 HashMap 的容量 ≥ 64 时,才会进行树化
    • 防止在小容量情况下频繁树化带来性能问题
  3. 链表结构变为“尾插法”而不是“头插法”

    • JDK7 中使用的是头插法,新元素放到链表头,导致顺序和插入顺序相反,在并发或 rehash 时容易死循环(比如多线程扩容下)
    • JDK8 中改成了尾插法,读取顺序和插入顺序一致,也避免的扩容死链问题。
  4. 重新设计了 Hash 函数(扰动函数)

    • 在 JDK8 中对 hash 值做了扰动处理,(右移16 位异或原始 hash)

      java
      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }

      这样做是为了让 hash 分布更加均匀,减少 hash 冲突

JDK8 HashMap为什么引入红黑树,而不是 AVL 树

在JDK8中,HashMap在解决哈希冲突时,当链表长度超过阈值(默认为8)时会将其转换为红黑树,而不是AVL树。这种设计选择主要基于以下几个原因:

  1. 性能权衡

    红黑树和AVL树都是自平衡二叉搜索树,但它们的平衡策略不同:

    • AVL树:严格平衡,任何节点的左右子树高度差不超过1
    • 红黑树:近似平衡,确保没有一条路径会比其他路径长出两倍

    这种差异导致了:

    • 插入/删除操作:红黑树通常更快,因为它的旋转操作更少
    • 查找操作:AVL树略快,因为它的平衡更严格

    在 HashMap 的场景中,插入和删除的操作比查找更频繁,因此红黑树更合适。

  2. 实现复杂度

    红黑树的实现相对 AVL 树更加简单,代码维护成本更低,HashMap 作为基础数据结构,实现复杂度是一个重要的考量因素

  3. 实际应用场景

    在 HashMap中:

    • 树结构的转换是相对罕见的(哈希函数良好时)
    • 即使转换为树,节点数量通常不会太大(超过阈值时会扩容)
    • 红黑树的近似平衡已经足够提供 O log(n)的性能
  4. 空间开销

    • 红黑树通常比 AVL 树需要更少的额外空间来维护平衡信息(红黑树只需要 1 位存储颜色信息,而 AVL 树需要存储平衡因子)。
  5. 统计性能

    在实际应用汇总,红黑树的统计性能更好,因为:

    • 对于随机插入的数据,红黑树的旋转操作更少
    • 对于有序数据,两者性能相当

JDK8 HashMap 为啥不一开始就直接使用红黑树

image-20250418001037106

因为树节点所占用的空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点,也就是说,最开始使用链表的时候,链表是比较短的,空间占用也是比较少的,查询性能都差不多,但是当链表越来越长,链表查询越来越慢,为了保证查询效率,才是设计链表而使用红黑树

HashMap 的 put 方法逻辑

先上一段源码

java
    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}.
     *         (A {@code null} return can also indicate that the map
     *         previously associated {@code null} with {@code key}.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
      	//如果表为空或长度为 0,则进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
      	//计算桶索引 (n-1)& hash
        if ((p = tab[i = (n - 1) & hash]) == null)
          	//如果桶为空,直接创建新节点
            tab[i] = newNode(hash, key, value, null);
        else {
          	//桶不为空
            Node<K,V> e; K k;
          	//检查第一个节点是否就是要找的键
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
              	//节点是红黑树,则调用红黑树的插入方法
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              	//节点是链表,遍历链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                      	//到达链表尾部,添加新节点
                        p.next = newNode(hash, key, value, null);
                      	//如果链表长度达到树化阈值(8),转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                  	//检查是否找到相同键
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
          	//如果已经找到键
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
              	//根据 onlyIfAbsent 决定是否替换旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
      	//如果 size 超过阈值,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

大概源码流程如下:

  1. 调用 putVal 方法将键值对插入到 HashMap中。
  2. 计算键的哈希值,如果键位 null,则哈希值为 0
  3. 根据哈希值找到键值对应的位置
    • 如果 HashMap 的 table 数组位 null 或长度为 0,则需要进行初始化;
    • 计算键值对在 table 数组中的位置,如果该位置上没有其他键值对,则直接插入键值对;
  4. 如果该位置已经存在其他键值对,则需要进行链表或红黑树的插入操作:
    • 如果该位置上已经存在相同的键,则直接用新的值替换旧的值;
    • 如果该位置上存在 TreeNode 节点,则将键值对插入到红黑树中;
    • 如果该位置上存在链表,则遍历链表找到合适的位置插入键值对;
    • 如果链表长度超过了阈值,则需要将链表转换为红黑树;
  5. 如果键已经存在,则用新的值替换旧的值。
  6. 插入成功,增减 modCount 和 size,如果 size 超过了阈值,则需要进行扩容。

HashMap 的 get 方法逻辑

先上源码

java
    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
           //检查第一个节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
              //如果是红黑树
                if (first instanceof TreeNode)
                  	//遍历红黑树
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
              	//遍历链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

基本流程如下

  1. 计算哈希值:首先对输入的 key 调用 hashCode()方法计算哈希值
  2. 确定桶的位置:通过哈希值与数组长度计算得到数组下标(桶的位置)
    1. 通常使用 (n-1) & hash 来计算(n)是数组长度
  3. 查找节点:
    1. 如果该桶位置只有一个节点,直接检查该节点的 key 是否匹配
    2. 如果是链表,则遍历链表查找匹配的 key
    3. 如果是红黑树,则调用红黑树的查找方法
  4. 返回结果:找到匹配的节点则返回其 value,否则返回 null

关键点

  1. 哈希冲突处理:使用链表或红黑树解决哈希冲突
  2. 性能考虑:理想情况下时间复杂度为O(1),最坏情况下为 O(Log n)(当转化为红黑树时)
  3. null 键处理,HashMap 允许一个 Null 键,会放在数组的第一个位置
  4. equals 方法:确定 key 是否相等时,先比较 hash 值,再使用 equals方法

HashMap 允许一个键为 null,null 键会放在数组的第一个位置,以下三种情况,HashMap会返回 null:

  1. table数组为 null 或者长度为 0;
  2. table 数组中不存在要查找的键;
  3. 键存在,但对应的值为 null;

HashMap 是先插入元素还是先扩容,为什么这么设计?是怎么扩容的?有没有容量限制

插入与扩容顺序

HashMap 是先插入元素再检查是否需要扩容。这种设计有以下原因:

  1. 效率考虑:先插入可以避免不必要的扩容检查。如果先检查扩容,可能在很多情况下即使不触发扩容也要进行检查。
  2. 准确性:只要在实际插入新元素(而非替换旧值)时才可能触发扩容,先插入可以准确判断是否真的增加了元素数量。
  3. 一致性:确保扩容时新元素已经存在于表中,保持数据一致性。

扩容机制

HashMap 扩容的主要过程:

  1. 触发条件:当元素数量超过阈值(容量 x 负载因子(默认0.75))时触发扩容。
  2. 扩容大小:通常扩容为原容量的 2 倍(保持为 2 的幂次方)
  3. 数据迁移:
    • 创建新数组(原大小的两倍)
    • 重新计算每个元素的位置(使用新的数组长度)
    • 处理哈希冲突(Java8 之后使用红黑树)
  4. java8优化:
    • 元素在新数组中的位置要么是原位置,要么是原位置+原容量
    • 减少了重新计算哈希值的开销

容量限制

  1. 最大容量:HashMap 的最大容量是 2^30(1073471824)
    • 定义在代码中 static final int MAXIMUM_CAPACITY = 1 <<30
    • 这是有数组索引使用 int 类型决定的
  2. 默认值:
    • 初始容量:16
    • 负载因子:0.75
  3. 自动调整:当构造 HashMap 时指定了初始容量,实际容量会别调整为不小于该值的下一个 2 的幂次方数

HashMap的插入与扩容代码解析

以 java8 为例

  1. 插入元素的核心流程

    插入元素的入口是 put()方法:

    java
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    实际工作在 putVal() 方法中完成:

    java
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 步骤1:如果表为空或长度为0,先进行扩容(初始化)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
        // 步骤2:计算索引位置,如果该位置为空,直接插入新节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 处理哈希冲突的情况...
        }
        
        // 步骤3:修改计数器并检查是否需要扩容
        ++modCount;
        if (++size > threshold)
            resize();  // 先插入元素,再检查扩容
        afterNodeInsertion(evict);
        return null;
    }
  2. 扩容机制代码解析

    扩容的核心方法是 resize():

    java
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        
        // 步骤1:计算新容量和新阈值
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 双倍扩容
        }
        else if (oldThr > 0) // 初始容量设为阈值
            newCap = oldThr;
        else {               // 使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        
        // 步骤2:创建新数组
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        
        // 步骤3:数据迁移(重新哈希)
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)  // 单个节点
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)  // 树节点
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {  // 链表节点
                        // 保持原有顺序
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 判断节点在新表中的位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 将链表放入新表
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  3. 关键设计点解析

    为什么先插入再扩容?

    从 putVal() 方法可以看到:

    java
    // 先插入元素
    tab[i] = newNode(hash, key, value, null);
    
    // 然后检查是否需要扩容
    if (++size > threshold)
        resize();

    这样设计的原因是:

    1. 减少不必要的扩容检查:只有当实际添加了新元素(size 增加)时才需要检查扩容
    2. 保持一致性:确保扩容时新元素已经在表中
    3. 性能优化:避免在替换现有键值时进行扩容检查

    扩容优化技巧

    java8 的优化体现在数据迁移时:

    java
    if ((e.hash & oldCap) == 0) {
        // 保持原索引位置
        newTab[j] = loHead;
    } else {
        // 新位置 = 原索引 + oldCap
        newTab[j + oldCap] = hiHead;
    }

    这种优化:

    1. 避免了重新计算每个元素的哈希值
    2. 利用哈希值位特性快速确定新位置
    3. 保持里链表的原始顺序(避免发生并发问题)

    容量限制实现

    在扩容时检查:

    java
    static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量定义
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;  // 不再扩容
    }
  4. 完整流程实例

    假设默认初始容量为 16,负载因子为 0.75:

    1. 第一次插入:table 为 null,触发 resize() 初始化 table 为 16
    2. 插入第 13 个元素时(16 x 0.75=12):
      1. 先插入第 13 个元素
      2. 然后发现 size=13 > threshold=12 ->触发扩容
      3. 容量从 16 扩容到 32,阈值变为 24
    3. 后续插入都遵循相同逻辑

TreeMap的数据结构是什么?

TreeMap是 java 集合框架中基于红黑树(Red-Black tree) 实现的 NavigableMap,他保证了元素按照键的自然顺序或者构造时提供的 Comparator 进行排序

  1. TreeMap 的核心结构

    java
    public class TreeMap<K,V>
        extends AbstractMap<K,V>
        implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
        
        // 比较器,用于确定键的顺序
        private final Comparator<? super K> comparator;
        
        // 红黑树的根节点
        private transient Entry<K,V> root;
        
        // 树中元素的数量
        private transient int size = 0;
        
        // 修改计数器(用于快速失败机制)
        private transient int modCount = 0;
        
        // 静态内部类表示树的节点
        static final class Entry<K,V> implements Map.Entry<K,V> {
            K key;
            V value;
            Entry<K,V> left;
            Entry<K,V> right;
            Entry<K,V> parent;
            boolean color = BLACK; // 红黑树节点颜色
            
            Entry(K key, V value, Entry<K,V> parent) {
                this.key = key;
                this.value = value;
                this.parent = parent;
            }
            
            public K getKey() { return key; }
            public V getValue() { return value; }
            public V setValue(V value) {
                V oldValue = this.value;
                this.value = value;
                return oldValue;
            }
        }
    }

    红黑树的五个性质

    1. 每个节点是红色或者黑色
    2. 根节点是黑丝
    3. 所有叶子节点(NIL)是黑色
    4. 红色节点的子节点必须是黑色(不呢有连续的红色节点)
    5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
  2. 核心操作代码解析

    在TreeMap 中,插入元素时,首先会根据键的大小确定元素插入的位置,然后执行插入操作。插入节点后,可能会破坏红黑树的平衡性,因此需要通过旋转和颜色调整来恢复平衡性

    插入操作

    java
    private V put(K key, V value, boolean replaceOld) {
      			//获取根据节点
            Entry<K,V> t = root;
      
     				//空值处理
            if (t == null) {
                addEntryToEmptyMap(key, value);
                return null;
            }
            int cmp;
            Entry<K,V> parent;
            // 判断是否使用自定义比较器
            Comparator<? super K> cpr = comparator;
      			//使用比较器排序
            if (cpr != null) {
              	//使用自定义比较器比较键值,并找到插入位置
                do {
                    parent = t;
                    cmp = cpr.compare(key, t.key);
                    if (cmp < 0)
                      	//如果新键小于当前节点键,移动到左子数
                        t = t.left;
                    else if (cmp > 0)
                     		//否则,移动到右子树 
                        t = t.right;
                    else {
                      	//如果键已经存在,决定是否替换旧值
                        V oldValue = t.value;
                        if (replaceOld || oldValue == null) {
                            t.value = value;//替换旧值
                        }
                        return oldValue;//返回旧值
                    }
                } while (t != null);
            } else {
              	//如果没有自定义比较器,使用键的自然排序
                Objects.requireNonNull(key);//确保键不为 null
                @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
                do {
                    parent = t;
                    cmp = k.compareTo(t.key);
                    if (cmp < 0)
                        t = t.left;//如果新建小于当前节点键,移动到左子树
                    else if (cmp > 0)
                        t = t.right;//否则移动到右子树
                    else {
                      	//如果键已经存在,决定是否替换旧值
                        V oldValue = t.value;
                        if (replaceOld || oldValue == null) {
                            t.value = value;//替换旧值
                        }
                        return oldValue;//返回旧值
                    }
                } while (t != null);
            }
      			//没有找到相同的键,插入新节点
            addEntry(key, value, parent, cmp < 0);
            return null;
        }

    红黑树平衡调整(fixAfterInsertion)

    java
      private void fixAfterInsertion(Entry<K,V> x) {
            x.color = RED;//新插入的节点总是红色
    				//只有当父节点也是红色时才需要调整
            while (x != null && x != root && x.parent.color == RED) {
                if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                    Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                    if (colorOf(y) == RED) {
                      	//情况 1 :叔叔节点是红色
                        setColor(parentOf(x), BLACK);
                        setColor(y, BLACK);
                        setColor(parentOf(parentOf(x)), RED);
                        x = parentOf(parentOf(x));
                    } else {
                        if (x == rightOf(parentOf(x))) {
                          	//情况 2:叔叔节点是黑色且当前节点是右孩子,需要左旋
                            x = parentOf(x);
                            rotateLeft(x);
                        }
                      //情况 3:叔叔节点是黑色且当前节点是左孩子,需要右旋
                        setColor(parentOf(x), BLACK);
                        setColor(parentOf(parentOf(x)), RED);
                        rotateRight(parentOf(parentOf(x)));
                    }
                } else {
                  	//对称的情况
                    Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                    if (colorOf(y) == RED) {
                        setColor(parentOf(x), BLACK);
                        setColor(y, BLACK);
                        setColor(parentOf(parentOf(x)), RED);
                        x = parentOf(parentOf(x));
                    } else {
                        if (x == leftOf(parentOf(x))) {
                            x = parentOf(x);
                            rotateRight(x);
                        }
                        setColor(parentOf(x), BLACK);
                        setColor(parentOf(parentOf(x)), RED);
                        rotateLeft(parentOf(parentOf(x)));
                    }
                }
            }
            root.color = BLACK;//确保根几点永远是黑色
        }

    查找操作

    java
    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }
    
    final Entry<K,V> getEntry(Object key) {
            // Offload comparator-based version for sake of performance
      			//如果有比较器,使用比较器查找
            if (comparator != null)
                return getEntryUsingComparator(key);
            Objects.requireNonNull(key);
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            Entry<K,V> p = root;
            while (p != null) {
                int cmp = k.compareTo(p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
            return null;
        }

什么是 ConcurrentHashMap?ConcurrentHashMap 的数据结构?

  1. 什么是ConcurrentHashMap?

    ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中的一个线程安全的哈希表实现,用于在多线程环境下高效地存储和访问键值对。相比于 HashtableCollections.synchronizedMap,它提供了更高的并发性能,采用分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8) 来减少锁竞争。

    主要特点

    • **线程安全:**支持高并发读写,不会抛出ConcurrentModificationException
    • **高性能:**比 HashtablesynchronizedMap 更高的吞吐量。
    • 弱一致性迭代器(Weakly Consistent Iterators):遍历时可能反映部分并发修改,但不会失败。
    • 不允许 null 键或值(与 HashMap 不同)。
  2. ConcurrentHashMap 的数据结构

    ConcurrentHashMap 的数据结构在 JDK 1.7JDK 1.8 中有较大变化:

    1. JDK 1.7:分段锁(Segment + HashEntry)

      采用 分段锁(Segment Locking) 机制,将整个哈希表分成多个 Segment(段),每个 Segment 是一个独立的 ReentrantLock,不同 Segment 可以并发操作,提高并发度。

      java
      // JDK 1.7 结构
      ConcurrentHashMap
          ├── Segment[] (默认 16 个 Segment)
          │   └── HashEntry[] (每个 Segment 维护一个哈希表)
          │       └── HashEntry<K,V> (链表节点)

      特点:

      • 锁粒度:锁的是 Segment,而不是整个表,减少锁竞争。
      • 并发度:默认 16 个 Segment,最多支持 16 个线程并发写。
      • 查询不加锁:读操作无需锁,仅写操作加锁。
    2. JDK 1.8:CAS + synchronized(Node + 红黑树)

      JDK 1.8 抛弃了分段锁,改为 CAS(Compare-And-Swap) + synchronized 优化锁粒度,并引入 红黑树 优化长链表查询性能。

      java
      // JDK 1.8 结构
      ConcurrentHashMap
          ├── Node<K,V>[] (哈希表数组)
          │   ├── Node<K,V> (链表节点)
          │   └── TreeBin<K,V> (红黑树节点, 当链表长度 ≥ 8 时转换)

      特点:

      • 锁粒度更细:仅锁单个桶(synchronized 锁住链表头或树根),并发度更高。
      • CAS 无锁化put 操作先尝试 CAS 插入,失败才加锁。
      • 红黑树优化:链表长度 ≥ 8 且桶数量 ≥ 64 时,链表转红黑树(提高查询效率)。
      • 扩容优化:支持多线程协助扩容(transfer 方法)。
    3. ConcurrentHashMap 核心方法实现

      1. JDK 1.7 的 put 流程
        1. 计算 key 的 hash,确定所属 Segment。
        2. 尝试获取该 Segment 的锁。
        3. 在 Segment 内部的 HashEntry 数组中找到对应桶,遍历链表:
          • 如果 key 已存在,更新 value。
          • 否则,插入新节点(头插法)。
        4. 释放锁。
      2. JDK 1.8 的 put 流程
        1. 计算 key 的 hash,定位到桶。
        2. 如果桶为空,CAS 插入新节点。
        3. 如果桶不为空:
          • synchronized 锁住链表头或树根。
          • 遍历链表/树:
            • 若 key 存在,更新 value。
            • 否则,插入新节点(尾插法)。
          • 如果链表长度 ≥ 8,尝试转红黑树。
        4. 检查是否需要扩容(sizeCtl 控制)。
    4. ConcurrentHashMap vs. HashMap vs. Hashtable

      特性ConcurrentHashMapHashMapHashtable
      线程安全✅(分段锁/CAS + synchronized)✅(全表锁 synchronized)
      并发性能⭐⭐⭐⭐(高)❌(非线程安全)⭐(低,全局锁)
      允许 null 键/值
      迭代器弱一致性❌(可能抛出 ConcurrentModificationException❌(强一致性)
    5. 总结

      • JDK 1.7:分段锁(Segment + HashEntry),锁粒度较粗,适合中等并发。
      • JDK 1.8CAS + synchronized + 红黑树,锁粒度更细,高并发性能更好。
      • 适用场景:高并发读写、缓存、共享数据存储(如 Spring Bean 容器)。

ConcurrentHashMap 是如何获取集合的准确大小的?

JAVA7 的实现方式

在 Java 7 中,ConcurrentHashMap 采用了一种特殊的方法来获取大小:

  1. 分段统计:首先不加锁尝试遍历所有段(Segment),累加各段的计数
  2. 比较前后结果:如果两次统计结果相同,则认为获取到了准确大小
  3. 加锁统计:如果前后结果不一致,则会对所有段加锁后重新统计

这种方法在大多数情况下能避免全局加锁,但在高并发环境下可能导致多次重试。

Java 8 及以后的实现

Java 8 对 ConcurrentHashMap 进行了重大重构,移除了分段锁设计,采用了更高效的大小统计方式:

  1. 基础计数器:使用 LongAdder 或类似的机制来维护基础计数器(baseCount)
  2. 计数器数组:当并发更新时,使用一个计数器数组(CounterCell[])来分散竞争
  3. 总和计算:size() 方法返回的是 baseCount 与所有 CounterCell 中值的总和

关键源码实现:

java
public int size() {
    long n = sumCount();//java 17 中直接调用的 mapcount
    return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

特点与注意事项

  1. 弱一致性:size() 返回的值是近似值,返回对象为一个 int 类型,因为在统计过程中可能有并发修改
  2. 高效性:相比 Java 7 减少了锁竞争,性能更高
  3. mappingCount():Java 8 新增了返回 long 类型的 mappingCount() 方法,推荐使用以避免整数溢出
  4. size()和 mappingCount()都是 弱一致性的,无法在多线程频繁插入/删除的情况下保持绝对准确的元素数量

使用建议

如果需要精确的实时大小,可以考虑:

  1. 在应用层维护独立计数器

  2. 在需要精确统计时暂停所有写操作

  3. 使用 mappingCount() 而非 size() 以避免整数溢出问题

  4. 强一致性代码参考

    java
    int accurateSize;
    synchronized (map) {
        accurateSize = 0;
        for (Map.Entry<K, V> entry : map.entrySet()) {
            accurateSize++;
        }
    }

常见的线程安全的 List 集合有:

同步包装类线程安全的 List(Synchronized Wrappers)

这些是通过 Collections.synchronizedXXX() 方法将普通集合包装成线程安全的:

  • Collections.synchronizedList(List<T>)
  • Collections.synchronizedMap(Map<K,V>)
  • Collections.synchronizedSet(Set<T>)

特点

  • 使用内部同步(synchronized 关键字)实现线程安全。
  • 每个方法调用都会加锁,性能较低
  • 适合低并发环境

并发集合类(java.util.concurrent包)

这些都是为高并发设计的线程安全的常见的集合类型

集合类型实现类特点/用途
ListCopyOnWriteArrayList写时复制,适合读多写少场景
SetCopyOnWriteArraySet基于 CopyOnWriteArrayList 实现
MapConcurrentHashMap分段锁/无锁设计,高并发性能优秀
QueueConcurrentLinkedQueue基于链表的无界队列,非阻塞
DequeConcurrentLinkedDeque双端无界队列,非阻塞
BlockingQueueArrayBlockingQueue有界阻塞队列,基于数组
BlockingQueueLinkedBlockingQueue有界或无界阻塞队列,基于链表
BlockingQueuePriorityBlockingQueue优先级阻塞队列
BlockingQueueDelayQueue延迟元素队列,用于定时任务调度等
BlockingQueueSynchronousQueue直接交付,不存储元素,用于线程之间传递
BlockingDequeLinkedBlockingDeque双端阻塞队列
Skip ListConcurrentSkipListMap / ConcurrentSkipListSet有序结构,适合范围查询,高并发有序操作

原子变量集合(Atomic 系列)

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

这些不是传统意义的集合,但提供对数组的原始访问能力。

使用建议

需求类型推荐集合
高并发读写 MapConcurrentHashMap
读多写少的 ListCopyOnWriteArrayList
阻塞式生产者消费者LinkedBlockingQueueArrayBlockingQueue
有序高并发 MapConcurrentSkipListMap

ArrayList 的默认大小是多少,如何进行扩容的?

默认构造函数

java
/**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

此时默认容量为:0(不是 10)但它会在第一次添加元素时变为 10

java
    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

扩容大小为多少

java21 源码

java
    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     * @throws OutOfMemoryError if minCapacity is less than zero
     */
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          	//扩容在原始基础上增加 0.5 倍,即扩容为原始大小的 1.5 倍
          	//如果扩容为原值的 1.5 倍后超过了容量最大值,每次扩容的值为 minCapacity - oldCapacity 的差值(java 21中应该是 1)
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
          	//初始扩容,容量由 0 扩容为 10
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // preconditions not checked because of inlining
        // assert oldLength >= 0
        // assert minGrowth > 0

        int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
            return prefLength;
        } else {
            // put code cold in a separate method
            return hugeLength(oldLength, minGrowth);
        }
    }

如何避免频繁扩容?

由于每次扩容时,都是调用一次Arrays.copyOf() 进行复制,比较耗费资源,初始化数组时,如果你知道大概的数据量范围,提前设置容量可以显著减少扩容次数,提高性能

java
private List<String> list = new ArrayList<>(1000);//初始化时提供初始值

ArrayList 为什么不是线程安全的?

ArrayList 不是线程安全的,这是因为它没有对关键的共享操作做同步处理。在多线程环境中,如果多个线程同时对同一个 ArrayList 实例进行读写操作,可能会导致数据错乱、异常甚至崩溃。

结构性修改不同步

ArrayList 的核心成员是一个普通的数组:

java
    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

常用的操作如 add(), remove(), get() 等,在源码中都**没有使用任何同步(如 synchronized 或 Lock)**来保护这些数组操作。

java
// 非线程安全的 add 方法简化逻辑
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 步骤1:检查容量
    elementData[size++] = e;          // 步骤2:赋值并增加size
    return true;
}

这两个步骤不是原子操作,多线程环境下可能导致:

  • 数据覆盖:两个线程同时执行步骤2时可能覆盖彼此的值
  • size不一致:size++不是原子操作(实际是读-改-写三步操作)

扩容机制不安全

扩容步骤 Grow() 包含多个非原子操作

java
// 非线程安全的扩容简化过程
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 计算新容量
    elementData = Arrays.copyOf(elementData, newCapacity); // 创建新数组并复制数据
}

多线程环境下可能导致:

  • 数据丢失:一个线程正在复制数据时,另一个线程修改了原数组
  • 数组越界:扩容过程中其他线程访问旧数组

迭代器快速失败(Fail-Fast)

ArrayList 的迭代器会在检测到并发修改时抛出 ConcurrentModificationException

java
final void checkForCommodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

modCount(修改计数器)没有同步保护,多线程操作时会导致意外抛出异常

什么是 fail-fast?什么是 fail-safe?

在编程和系统设计中,fail-fast(快速失败)fail-safe(安全失败) 是两种处理错误或异常的机制,目标不同,行为也相反。

Fail-Fast(快速失败)

  • 定义:在程序运行中,一旦检测到错误或异常,立即抛出异常并停止当前操作

  • 目的:尽早发现问题,避免错误传播,便于调试。

  • 特点

    • 快速响应异常。

    • 提高程序的健壮性和调试效率。

    • 通常会抛出异常(如 Java 中的 ConcurrentModificationException)。

  • 常见场景:

    • Java 中的集合类(如 ArrayListHashMap)的 迭代器 Iterator 是 fail-fast 的

      java
      List<String> list = new ArrayList<>();
      list.add("A");
      list.add("B");
      
      for (String s : list) {
          list.remove(s); // 抛出 ConcurrentModificationException
      }

Fail-Safe(安全失败)

Fail-Safe 是一种避免失败而不是立即报告失败的策略。

特点:

  • 在遇到问题时不会抛出异常,而是尝试继续操作

  • Java 并发集合(如 CopyOnWriteArrayList, ConcurrentHashMap)采用此策略

  • 通常通过数据副本或特殊并发控制实现

  • 设计目的是保证系统在错误发生时仍能继续运行

  • 示例:

    java
    List<String> list = new CopyOnWriteArrayList<>();//CopyOnWriteArrayList 是 fail-safe 的,迭代器基于快照,修改不会影响正在进行的迭代,也不会抛异常。所以CopyOnWriteArrayList 是线程安全的
    list.add("A");
    list.add("B");
    
    for (String s : list) {
        list.remove(s); // 不会抛出异常
    }

关键区别

特性Fail-FastFail-Safe
错误处理立即抛出异常尝试继续操作
性能影响无额外开销可能有复制数据的开销
使用场景单线程环境多线程环境
集合示例ArrayList, HashMapCopyOnWriteArrayList, ConcurrentHashMap
数据一致性可能不一致(被中断)保持一致性

TreeSet 的数据结构是什么?

TreeSet 是 Java 中基于 红黑树(Red-Black Tree) 实现的集合类。

核心数据结构:红黑树(Red-Black Tree)

  • TreeSet 底层使用的是 TreeMap,而 TreeMap 是一个基于红黑树的有序映射结构。
  • 因此,TreeSet 实际上是用 TreeMap 来存储元素,只不过键是元素本身,值是一个常量(如 PRESENT 占位符)。

红黑树的特点

红黑树是一种 自平衡的二叉查找树(BST),它在插入和删除元素后,仍能保持树的平衡,从而保证 查找、插入、删除操作的时间复杂度都是 O(log n)

红黑树具备以下几个性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点永远是黑色。
  3. 所有叶子节点(null)都是黑色。
  4. 如果一个节点是红色的,则它的子节点必须是黑色的(即不能有两个连续的红色节点)。
  5. 从任一节点到其所有后代叶子节点的路径上,黑色节点数量相同(黑高一致)。

这些规则确保了树不会退化成链表,从而维持操作效率。

TreeSet 的行为特性(来源于红黑树)

特性表现
元素有序根据自然顺序(Comparable)或自定义 Comparator 排序
不允许重复元素判断依据是元素的比较结果为 0(不是 equals()
操作复杂度插入、删除、查找都是 O(log n)
支持范围操作subSet(), headSet(), tailSet()
支持排序遍历迭代出来的顺序是排序后的顺序

示例代码

java
import java.util.TreeSet;

public class TreeSetExample {
    public static void main(String[] args) {
        TreeSet<Integer> set = new TreeSet<>();
        set.add(5);
        set.add(2);
        set.add(8);
        set.add(1);

        System.out.println(set); // 输出:[1, 2, 5, 8],有序
    }
}

怎么确保一个集合不能被修改?

使用 Collections.unmodifiableXXX()

Java 提供了标准包装方法使集合变为只读:

java
import java.util.*;

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");

List<String> unmodifiableList = Collections.unmodifiableList(list);
unmodifiableList.add("c"); // 抛出 UnsupportedOperationException

⚠️ 注意:原始集合如果被修改,包装集合也会反映变化

使用 Java 9+ 的 List.of(), Set.of(), Map.of()

这些方法返回真正的不可变集合:

java
List<String> list = List.of("a", "b"); // 不可变
list.add("c"); // 抛出 UnsupportedOperationException
  • 优点:原始集合不能修改,也不能反射外部集合的变化。
  • 缺点:只在 Java9 及以上版本支持

使用第三方库(如 Google Guava)

java
import com.google.common.collect.ImmutableList;

List<String> immutableList = ImmutableList.of("a", "b");
immutableList.add("c"); // 抛出异常

Guava 的 ImmutableList 等类是真正不可变的、线程安全的。

常见场景及理由

场景原因
防止方法外部修改集合避免暴露内部状态,增强封装性
多线程场景下共享集合不可变集合天然线程安全
作为常量集合避免初始化后被意外改动

为什么不建议使用双括号初始化集合?

双括号初始化(Double Brace Initialization)是一种看起来简洁的集合初始化方式,但存在几个重要问题,因此不建议使用。

java
List<String> list = new ArrayList<String>() {{
    add("A");
    add("B");
    add("C");
}};

外层大括号创建匿名子类,内层大括号是实例初始化块(instance initializer block)

主要问题

  1. 性能问题

    • 每次使用都会创建一个新的匿名子类
    • 增加类加载开销(类加载器需要处理额外的类)
    • 生成的.class文件会增多
  2. 内存泄漏风险

    • 匿名内部类隐式持有外部类的引用
    • 如果集合被长期持有,会导致外部类无法被GC回收
  3. 序列化问题

    • 匿名类的序列化行为可能与预期不同
    • 反序列化时可能遇到问题
  4. 代码可读性

    • 看似简洁,但实际语法晦涩
    • 许多开发者不熟悉这种写法
    • IDE静态分析工具可能无法正确处理
  5. equals()方法行为

    • 因为创建的是匿名子类,所以 getClass() 检查会失败

      java
      new ArrayList<String>() {{ add("A"); }}.equals(new ArrayList<String>() {{ add("A"); }})
      // 返回false,因为它们是不同的类

推荐替代方案

  1. Java 9+ 的工厂方法

    java
    List<String> list = List.of("A", "B", "C");
    Set<String> set = Set.of("A", "B", "C");
    Map<String, Integer> map = Map.of("A", 1, "B", 2);
  2. Guava 库(Java 8及之前):

    java
    List<String> list = ImmutableList.of("A", "B", "C");
    Set<String> set = ImmutableSet.of("A", "B", "C");
  3. 传统方式

    java
    List<String> list = new ArrayList<>();
    list.add("A");
    list.add("B");
    list.add("C");
  4. 使用工具方法

    java
    List<String> list = Arrays.asList("A", "B", "C"); // 固定大小列表

什么是阻塞队列?

阻塞队列是一种特殊的队列,它在多线程编程中扮演着重要角色,提供了线程安全的入队和出队操作,并具有阻塞特性。

核心特性

  1. 线程安全:所有操作都是原子性的,多线程访问无需额外同步
  2. 阻塞操作
    • 当队列满时,插入操作会被阻塞,直到队列有空间
    • 当队列空时,获取操作会被阻塞,直到队列有元素
  3. 容量限制:可以是有界队列(固定容量)或无界队列(理论上无限容量)

Java 中的主线实现类

Java在java.util.concurrent包中提供了多种阻塞队列实现:

  1. ArrayBlockingQueue:基于数组的有界阻塞队列(FIFO)
  2. LinkedBlockingQueue:基于链表的可选有界阻塞队列(FIFO)
  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  4. DelayQueue:元素只有到期才能取出的无界阻塞队列
  5. SynchronousQueue:不存储元素的阻塞队列(直接传递)
  6. LinkedTransferQueue:改进的传输队列(Java 7+)

核心方法

阻塞队列提供四种不同的操作方法:

操作类型抛出异常返回特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()不可用不可用

典型应用场景

  1. 生产者消费者模式:

    java
    // 生产者
    public void produce() throws InterruptedException {
        while (true) {
            Object item = generateItem();
            queue.put(item);  // 队列满时阻塞
        }
    }
    
    // 消费者
    public void consume() throws InterruptedException {
        while (true) {
            Object item = queue.take();  // 队列空时阻塞
            processItem(item);
        }
    }
  2. 线程池任务队列(如ThreadPoolExecutor使用阻塞队列)

  3. 异步消息处理系统

  4. 流量控制和缓冲

工作原理示例(ArrayBlockingQueue)

  1. 内部结构:

    • 使用数组存储元素
    • 维护两个指针:putIndex(插入位置)和takeIndex(取出位置)
    • 使用ReentrantLock保证线程安全
    • 使用Condition实现阻塞通知机制
  2. put()方法代码

    java
        /**
         * Inserts the specified element at the tail of this queue, waiting
         * for space to become available if the queue is full.
         *
         * @throws InterruptedException {@inheritDoc}
         * @throws NullPointerException {@inheritDoc}
         */
        public void put(E e) throws InterruptedException {
            Objects.requireNonNull(e);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == items.length) //队列满
                    notFull.await();// 等待"不满"信号
                enqueue(e);//入队
            } finally {
                lock.unlock();
            }
        }
        /**
         * Inserts element at current put position, advances, and signals.
         * Call only when holding lock.
         */
        private void enqueue(E e) {
            // assert lock.isHeldByCurrentThread();
            // assert lock.getHoldCount() == 1;
            // assert items[putIndex] == null;
            final Object[] items = this.items;
            items[putIndex] = e;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notEmpty.signal(); //发送队列不空信号
        }

阻塞队列是线程安全的吗?

阻塞队列(Blocking Queue)是线程安全的,这是它的核心特性之一。

线程安全实现机制

阻塞队列通过以下几种方式保证线程安全:

  1. 内部锁机制

    • 使用ReentrantLocksynchronized保证操作的原子性

    • 例如ArrayBlockingQueue使用ReentrantLock:

      java
      final ReentrantLock lock = new ReentrantLock();
  2. 条件变量(Condition)

    • 实现精确的线程等待/唤醒机制

    • 通常包含两个Condition:

      java
      private final Condition notEmpty = lock.newCondition();  // 队列非空条件
      private final Condition notFull = lock.newCondition();   // 队列非满条件
  3. 线程安全的操作设计:

    • 所有 public 方法都内置同步控制

    • 例如put()take()方法的典型实现:

      java
      public void put(E e) throws InterruptedException {
          final ReentrantLock lock = this.lock;
          lock.lockInterruptibly();
          try {
              while (count == items.length)
                  notFull.await();
              enqueue(e);
          } finally {
              lock.unlock();
          }
      }

线程安全操作示例

方法类型线程安全保证典型方法
插入队列满时阻塞等待put(e), offer(e, timeout, unit)
移除队列空时阻塞等待take(), poll(timeout, unit)
检查非阻塞安全访问peek(), size()

为什么需要线程安全?

  1. 生产者-消费者模式:多线程并发操作队列时不需要额外同步

    java
    // 生产者线程
    new Thread(() -> {
        blockingQueue.put(data);  // 无需额外同步
    }).start();
    
    // 消费者线程
    new Thread(() -> {
        Data data = blockingQueue.take();  // 无需额外同步
    }).start();
  2. 避免竞态条件:防止多个线程同时修改导致数据不一致

  3. 内存可见性:通过volatile变量或锁保证修改对所有线程立即可见

注意事项

  1. 批量操作:如drainTo()方法虽然是原子的,但操作期间其他线程的修改不可见

  2. 迭代器:迭代器是弱一致性的(不保证反映所有最新修改)

  3. 特殊方法

    • remove(Object)等非阻塞方法也是线程安全的

    • 但组合操作(如"检查再行动")仍需外部同步:

      java
      if (!queue.isEmpty()) {  // 这两行不是原子操作
          queue.poll();        // 可能需要外部同步
      }

java中常见的阻塞队列有哪些?阻塞队列有哪些常用的应用场景?

java 中常见的阻塞队列

Java在java.util.concurrent包中提供了多种阻塞队列实现,以下是主要的几种:

  1. ArrayBlockingQueue
    • 基于数组实现的 有界阻塞队列
    • 按照 FIFO(先进先出)原则排序元素
    • 必须指定容量,创建后不能调整大小
    • 使用单个 ReentrantLock 控制 出入队操作
  2. LinkedBlockingQueue
    • 基于链表实现的可选有界阻塞队列
    • 默认容量为 Integer.MAX_VALUE(近似无界)
    • 使用分离的读写锁(putLock和takeLock)提高吞吐量
    • 同样遵循FIFO原则
  3. PriorityBlockingQueue
    • 支持优先级排序的无界阻塞队列
    • 基于二叉堆实现,默认自然排序或通过 comparator 比较器自定义排序
    • 自动扩容(最大为Integer.MAX_VALUE)
  4. DelayQueue
    • 基于PriorityQueue实现的无界延迟队列
    • 元素必须实现Delayed接口
    • 只有延迟期满的元素才能被取出
  5. SynchronousQueue
    • 不存储元素的特殊阻塞队列
    • 每个插入操作必须等待对应的移除操作
    • 支持公平和非公平两种模式
    • 吞吐量通常高于ArrayBlockingQueue和LinkedBlockingQueue
  6. LinkedTransferQueue
    • 基于链表的无界TransferQueue
    • 新增transfer()和tryTransfer()方法
    • 可以直接将元素传递给等待的消费者
  7. LinkedBlockingDeque
    • 双向阻塞队列,两端都可插入和移除
    • 减少多线程竞争,适合"工作窃取"模式
    • 可选有界(默认Integer.MAX_VALUE)

阻塞队列的常用应用场景

  1. 生产者-消费者模式
    • 解耦生产者和消费者,平衡处理速度差异
    • 生产者通过put()添加数据,消费者通过take()获取数据
    • 队列满时阻塞生产者,队列空时阻塞消费者
  2. 线程池任务队列
    • Java线程池(如ThreadPoolExecutor)使用阻塞队列作为工作队列
    • 当核心线程忙时,新任务进入队列等待
    • 队列满时根据策略处理(拒绝或创建新线程)
  3. 异步消息处理系统
    • 不同线程间安全传递消息
    • 发送方put消息,接收方take消息
    • 特别适合事件驱动架构
  4. 定时任务调度
    • DelayQueue可用于实现定时任务系统
    • 任务按执行时间排序,到期自动触发
    • 如TimerQueue和ScheduledThreadPoolExecutor内部实现
  5. 缓存系统设计
    • 使用DelayQueue管理缓存过期
    • 后台线程定期检查并移除过期缓存
    • 元素过期时间作为延迟时间
  6. 流量控制和缓冲
    • 控制突发流量,防止系统过载
    • 作为缓冲区平衡生产消费速率差异
    • 如文件监控系统中处理文件变化事件
  7. 高并发数据传输
    • SynchronousQueue适合线程间直接传递数据
    • 无中间存储,减少内存开销
    • 如CachedThreadPool使用它处理新任务

阻塞队列通过内置的线程安全机制和阻塞/唤醒功能,极大简化了多线程编程的复杂度,是Java并发包中最重要的工具之一。选择哪种阻塞队列取决于具体需求,如是否需要边界、排序、延迟等特性

本站访客数 人次 本站总访问量