【2025最新Java面试八股】Java中实现线程安全的集合?ConcurrentHashMap是如何保证线程安全的?List.of()和subLIst底层是怎么样的?COW又是什么?

在 Java 中,实现线程安全的集合有多种方式,主要分为两大类:基于锁的同步集合并发优化的无锁/分段锁集合。以下是详细说明和 ConcurrentHashMap 的线程安全实现原理。


一、Java 中实现线程安全的集合的几种方式

1. 同步包装类(基于锁)

通过 Collections.synchronizedXXX 方法将普通集合转为线程安全集合,底层使用 互斥锁(synchronized) 保证线程安全:

List syncList = Collections.synchronizedList(new ArrayList<>());
Map syncMap = Collections.synchronizedMap(new HashMap<>());
Set syncSet = Collections.synchronizedSet(new HashSet<>());

特点

  • 简单直接,所有方法加锁,保证强一致性。

  • 性能差:高并发时锁竞争激烈。

2:在调用集合前,使用synchronized或者ReentrantLock对代码加锁(读写都要加锁)

public class SynchronizedCollectionExample {

                private List list = new ArrayList<>();

                public void add(int value) {

                       //加锁

                        synchronized (SynchronizedCollectionExample.class) {

                                        list.add(value);

                        }

                }

       

                public int get(int index) {

                       //加锁

                        synchronized (SynchronizedCollectionExample.class) {

                                return list.get(index);

                        }

                }

}

3:使用ThreadLocal,将集合放到线程内访问,但是这样集合中的值就不能被其他线程访问了

public class ThreadLocalCollectionExample {

        //初始化

        private ThreadLocal> threadLocalList =                 ThreadLocal.withInitial(ArrayList::new);

                public void add(int value) {

                        threadLocalList.get().add(value);

                }

                public int get(int index) {

                        return threadLocalList.get().get(index);

                }

}

4:使用不可变集合进行封装,当集合是不可变的时候,自然是线程安全的

import com.google.common.collect.ImmutableList;

public class ImmutableCollectionExample {

               //使用不可变集合初始化

                private ImmutableList immutableList = ImmutableList.of(1, 2, 3);

                public int get(int index) {

                        return immutableList.get(index);

                }

}

import java.util.List;

public class ImmutableJava9Example {

                //List.of()返回的也是不可变的集合(后面底层实现单独说)

                private List immutableList = List.of(1, 2, 3);

                public int get(int index) {

                        return immutableList.get(index);

                }

}

5. 并发集合(JUC 包)

Java 并发包(java.util.concurrent)提供了高性能的线程安全集合:

Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。
Java并发集合类主要包含以下几种:
1ConcurrentHashMap
2ConcurrentLinkedDeque
3ConcurrentLinkedQueue
4ConcurrentSkipListMap
5ConcurrentSkipListSet
6CopyOnWriteArrayList
7CopyOnWriteArraySet

下面是常用的

集合类型 实现类 特点
Map ConcurrentHashMap 分段锁 + CAS,高并发优化
List CopyOnWriteArrayList 写时复制(适合读多写少)
Set CopyOnWriteArraySet 基于 CopyOnWriteArrayList
Queue ConcurrentLinkedQueue 无锁(CAS 实现),高性能非阻塞队列
BlockingQueue ArrayBlockingQueue 基于锁的阻塞队列(固定容量)

二、ConcurrentHashMap 的线程安全实现原理

ConcurrentHashMap(JDK 1.7 和 JDK 1.8 实现不同)是线程安全的哈希表,其核心设计目标是 高并发读写

并且ConcurrentHashMap是不允许null的key和value的,因为具有模糊性(二义性)。

1. JDK 1.7:分段锁(Segment)(了解即可,性能不高)

,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。

  • 数据结构:将哈希表分成多个 Segment(默认 16 个),每个 Segment 是一个独立的 HashEntry 数组。

  • 锁机制

    • 写操作:只需锁住对应的 Segment,其他 Segment 仍可并发访问。

    • 读操作:无锁(使用volatile 保证可见性)。

  • 缺点:分段数固定,并发度受限于 Segment 数量。分段锁提高了并发性,但在段数固定的情况下,并发很高的时候仍可能导致热点段,从而成为性能瓶颈。另外,由于每个段都是独立的结构,这可能导致较高的内存占用。
    所以,JDK 1.8 对 ConcurrentHashMap 的实现进行了重大改进,不再使用分段锁,而是采用了一种基于节点锁的方法,并且在内部大量使用了 CAS 操作来管理状态。

2. JDK 1.8:CAS + synchronized 优化(性能较高,常用)

JDK 1.8 抛弃分段锁,优点如下:

更细的锁粒度:通过对单个节点的锁定而不是整个段,大幅降低了锁的竞争。
●CAS 操作:对数据结构的很多更新操作使用无锁的 CAS 操作,提高了效率,尤其是在读多写少的场景下。
●性能和扩展性:Java 1.8 的实现在高并发环境下提供了更好的性能,特别是通过减少锁的竞争和提高数据结构的效率。
●内存效率:Java 1.8 的实现通过减少锁的数量和使用更简洁的数据结构,提高了内存效率。

(1)数据结构
  • 使用 Node 数组 + 链表/红黑树(同 HashMap)。

  • 关键字段:

    transient volatile Node[] table; // 哈希桶数组(volatile 保证可见性)
    private transient volatile int sizeCtl; // 控制表初始化和扩容
(2)线程安全机制

主要是解决初始化桶数组阶段设置桶,插入,树化等阶段的并发问题。

操作 实现方式
初始化 Map在此处用CAS自旋操作,如果此时没有线程初始化,则去初始化,否则当前线程让出CPU时间片,等待下一次唤醒。
插入/更新 桶为空:CAS 插入新节点。
桶非空:锁住链表头节点(synchronized)。
扩容 多线程协同扩容,通过 sizeCtl (如果其为负数,则说明有多线程在操作,且Math.abs(sizeCtl)即为线程的数目。)和 CAS 控制。
(3)关键代码分析(JDK 1.8以后)

put阶段时,

如果hash后发现桶中没有值,则会直接采用CAS插入并且返回
如果发现桶中有值,则对流程按照当前的桶节点为维度进行加锁,将值插入链表或者红黑树中,

以 put和putVal 方法为例:

  public V put(K key, V value) {

        return putVal(key, value, false);

  }

final V putVal(K key, V value, boolean onlyIfAbsent) {
    Node[] tab; Node p; int n, i;
    // 1. 表为空则初始化(CAS 保证只有一个线程初始化)
    if ((tab = table) == null || (n = tab.length) == 0)
        tab = initTable();
    // 2. 计算哈希位置,若桶为空则 CAS 插入
    else if ((p = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null, new Node(hash, key, value)))
            break;
    }
    // 3. 桶非空则锁住链表头节点
    else {
        synchronized (p) {
            // 处理链表或红黑树插入
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 遍历链表插入
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = new Node(hash, key, value);
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash); // 链表转红黑树
                        break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
        }
    }
}

过程是这样的。如果某个段为空,那么使用CAS操作来添加新节点;如果某个段中的第一个节点的hash值为MOVED,表示当前段正在进行扩容操作,那么就调用helpTransfer方法来协助扩容;否则,使用Synchronized锁住当前节点,然后进行节点的添加操作。

扩容时:

多线程最大的好处就是可以充分利用CPU的核数,带来更高的性能,所以ConcurrentHashMap并没有一味的通过CAS或者锁去限制多线程,在扩容阶段,ConcurrentHashMap就通过多线程来加加速扩容。
如下扩容流程:
1:通过CPU核数为每个线程计算划分任务,每个线程最少的任务是迁移16个桶
2:将当前桶扩容的索引transferIndex赋值给当前线程,如果索引小于0,则说明扩容完毕,结束流程,否则
3:再将当前线程扩容后的索引赋值给transferIndex,譬如,如果transferIndex原来是32,那么赋值之后transferIndex应该变为16,这样下一个线程就可以从16开始扩容了。这里有一个小问题,如果两个线程同时拿到同一段范围之后,该怎么处理?答案是ConcurrentHashMap会通过CAS对transferIndex进行设置,只可能有一个成功,所以就不会存在上面的问题
4:之后就可以对真正的扩容流程进行加锁操作了


三、对比其他线程安全 Map

实现类 锁粒度 读性能 写性能 适用场景
Hashtable 全局锁(方法级) 遗留代码,不推荐
Collections.synchronizedMap 全局锁(对象级) 简单场景
ConcurrentHashMap 桶级锁(JDK8) 高并发读写(推荐)

四:List.of()方法和subList()方法实现原理

JDK 9 引入List.of(),创建轻量级不可变集合,优化内存和线程安全。

实现原理
  1. 底层存储

    • 元素数量 ≤ 2:使用专用内部类(如 List12)直接存储字段。

    • 元素数量 ≥ 3:使用 ImmutableCollections.ListN 存储到数组。

    // JDK 源码片段
    static  List of(E e1, E e2, E e3) {
        return new ImmutableCollections.ListN<>(e1, e2, e3);
    }
  2. 不可变性

    • 所有修改方法(如 addremove)直接抛出 UnsupportedOperationException

    • 数组字段用 final 修饰,防止被替换。

  3. 优化

    • 避免装箱:对基本类型有重载方法(如 List.of(int...))。

    • 空集合复用:List.of() 返回共享的空集合实例。

特点
特性 说明
不可变 创建后无法修改
线程安全 因不可变,天然线程安全
内存优化 小集合用字段存储,大集合用数组
快速失败 修改操作直接抛出异常

3. subList() 方法的实现原理

提供原列表的视图,需注意修改的副作用和生命周期管理。

  • 需要临时操作列表的子集 → subList()(谨慎处理并发修改)。

  • List的subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示

作用
  • 返回原列表的视图(View),而非独立副本。

  • 对子列表的修改会影响原列表,反之亦然。

    List list = new ArrayList<>(List.of("A", "B", "C", "D"));
    List subList = list.subList(1, 3); // ["B", "C"]
    subList.set(0, "X"); // 原列表变为 ["A", "X", "C", "D"]
实现原理(以 ArrayList 为例)
  1. 数据结构

    • SubList 是 ArrayList 的内部类,持有原列表的引用和偏移量

    private class SubList extends AbstractList {
        private final AbstractList parent; // 原列表
        private final int offset; // 起始索引
        private int size; // 子列表大小
        // ...
    }

    底层原理,

    这个方法返回了一个SubList,这个类是ArrayList中的一个内部类。

    public List subList(int fromIndex, int toIndex) {

            subListRangeCheck(fromIndex, toIndex, size);

            return new SubList(this, 0, fromIndex, toIndex);

    }

  2. 操作委托

    • 所有方法(如 getset)通过偏移量映射到原列表:

      public E get(int index) {
          rangeCheck(index);
          return parent.get(index + offset);
      }
  3. 注意事项

    • 1、对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。

      2、对子List做结构性修改,操作同样会反映到父List上。

      3、对父List做结构性修改,会抛出异常ConcurrentModificationException。

      结构性修改:在集合中增加或者删除元素
      非结构性:在集合中修改某个元素的内容

    • 内存泄漏风险:子列表持有原列表引用,可能阻止原列表被 GC 回收。

  4. 如果需要对subList作出修改,又不想动原list。那么可以创建subList的一个拷贝

  5. subList = Lists.newArrayList(subList);

    list.stream().skip(start).limit(end).collect(Collectors.toList());

与 List.of() 的对比
特性 subList() List.of()
可变性 是原列表的视图,可修改 完全不可变
独立性 依赖原列表 独立数据存储
线程安全 依赖原列表的实现 天然线程安全
内存占用 低(仅存储引用) 可能较高(需存储数据副本)

五:COW是什么?

Copy-On-Write简称COW,其基本思想是读写分离,当想要修改这个内容的时候,把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。相当于线程安全的集合,还不需要加锁。

主要用于读多写少的并发场景。

CopyOnWriteArrayList的整个add方法操作都是在锁的保护下进行的。

也就是说add方法是线程安全的。

缺点:内存占用高(频繁复制)

          写操作开销大(需复制全量数据)

           数据实时性差(读操作看到的是旧数据)

六、总结

  1. 线程安全集合选择

    • 高并发读写:优先选 ConcurrentHashMapConcurrentLinkedQueue

    • 读多写少:考虑 CopyOnWriteArrayList

  2. ConcurrentHashMap 线程安全的核心

    • JDK 1.7:分段锁降低冲突。

    • JDK 1.8:CAS + synchronized 锁单个桶,进一步优化并发度。

  3. 注意事项

    • 即使使用线程安全集合,复合操作(如 putIfAbsent)仍需额外同步。

    • 迭代器是弱一致性的(反映创建时的状态)。

你可能感兴趣的:(java,面试,安全)