在 Java 中,实现线程安全的集合有多种方式,主要分为两大类:基于锁的同步集合和并发优化的无锁/分段锁集合。以下是详细说明和 ConcurrentHashMap
的线程安全实现原理。
通过 Collections.synchronizedXXX
方法将普通集合转为线程安全集合,底层使用 互斥锁(synchronized) 保证线程安全:
ListsyncList = Collections.synchronizedList(new ArrayList<>()); Map syncMap = Collections.synchronizedMap(new HashMap<>()); Set syncSet = Collections.synchronizedSet(new HashSet<>());
特点:
简单直接,所有方法加锁,保证强一致性。
性能差:高并发时锁竞争激烈。
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);
}
}
}
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);
}
}
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);
}
}
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
(JDK 1.7 和 JDK 1.8 实现不同)是线程安全的哈希表,其核心设计目标是 高并发读写。
并且ConcurrentHashMap是不允许null的key和value的,因为具有
模糊性(二义性)。
,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。
数据结构:将哈希表分成多个 Segment(默认 16 个),每个 Segment 是一个独立的 HashEntry
数组。
锁机制:
写操作:只需锁住对应的 Segment,其他 Segment 仍可并发访问。
读操作:无锁(使用volatile
保证可见性)。
缺点:分段数固定,并发度受限于 Segment 数量。分段锁提高了并发性,但在段数固定的情况下,并发很高的时候仍可能导致热点段,从而成为性能瓶颈。另外,由于每个段都是独立的结构,这可能导致较高的内存占用。
所以,JDK 1.8 对 ConcurrentHashMap 的实现进行了重大改进,不再使用分段锁,而是采用了一种基于节点锁的方法,并且在内部大量使用了 CAS 操作来管理状态。
JDK 1.8 抛弃分段锁,优点如下:
更细的锁粒度:通过对单个节点的锁定而不是整个段,大幅降低了锁的竞争。
●CAS 操作:对数据结构的很多更新操作使用无锁的 CAS 操作,提高了效率,尤其是在读多写少的场景下。
●性能和扩展性:Java 1.8 的实现在高并发环境下提供了更好的性能,特别是通过减少锁的竞争和提高数据结构的效率。
●内存效率:Java 1.8 的实现通过减少锁的数量和使用更简洁的数据结构,提高了内存效率。
使用 Node
数组 + 链表/红黑树(同 HashMap
)。
关键字段:
transient volatile Node[] table; // 哈希桶数组(volatile 保证可见性) private transient volatile int sizeCtl; // 控制表初始化和扩容
主要是解决初始化桶数组阶段和设置桶,插入,树化等阶段的并发问题。
操作 | 实现方式 |
---|---|
初始化 | Map在此处用CAS自旋操作,如果此时没有线程初始化,则去初始化,否则当前线程让出CPU时间片,等待下一次唤醒。 |
插入/更新 | - 桶为空:CAS 插入新节点。 - 桶非空:锁住链表头节点( synchronized )。 |
扩容 | 多线程协同扩容,通过 sizeCtl (如果其为负数,则说明有多线程在操作,且Math.abs(sizeCtl)即为线程的数目。)和 CAS 控制。 |
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:之后就可以对真正的扩容流程进行加锁操作了
实现类 | 锁粒度 | 读性能 | 写性能 | 适用场景 |
---|---|---|---|---|
Hashtable |
全局锁(方法级) | 差 | 差 | 遗留代码,不推荐 |
Collections.synchronizedMap |
全局锁(对象级) | 差 | 差 | 简单场景 |
ConcurrentHashMap |
桶级锁(JDK8) | 高 | 高 | 高并发读写(推荐) |
JDK 9 引入List.of(),创建轻量级不可变集合,优化内存和线程安全。
底层存储:
元素数量 ≤ 2:使用专用内部类(如 List12
)直接存储字段。
元素数量 ≥ 3:使用 ImmutableCollections.ListN
存储到数组。
// JDK 源码片段 staticList of(E e1, E e2, E e3) { return new ImmutableCollections.ListN<>(e1, e2, e3); }
不可变性:
所有修改方法(如 add
、remove
)直接抛出 UnsupportedOperationException
。
数组字段用 final
修饰,防止被替换。
优化:
避免装箱:对基本类型有重载方法(如 List.of(int...)
)。
空集合复用:List.of()
返回共享的空集合实例。
特性 | 说明 |
---|---|
不可变 | 创建后无法修改 |
线程安全 | 因不可变,天然线程安全 |
内存优化 | 小集合用字段存储,大集合用数组 |
快速失败 | 修改操作直接抛出异常 |
subList()
方法的实现原理提供原列表的视图,需注意修改的副作用和生命周期管理。
需要临时操作列表的子集 → subList()
(谨慎处理并发修改)。
List的subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示
返回原列表的视图(View),而非独立副本。
对子列表的修改会影响原列表,反之亦然。
Listlist = 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
为例)数据结构:
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);
}
操作委托:
所有方法(如 get
、set
)通过偏移量映射到原列表:
public E get(int index) { rangeCheck(index); return parent.get(index + offset); }
注意事项:
1、对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。
2、对子List做结构性修改,操作同样会反映到父List上。
3、对父List做结构性修改,会抛出异常ConcurrentModificationException。
结构性修改:在集合中增加或者删除元素
非结构性:在集合中修改某个元素的内容
内存泄漏风险:子列表持有原列表引用,可能阻止原列表被 GC 回收。
如果需要对subList作出修改,又不想动原list。那么可以创建subList的一个拷贝:
subList = Lists.newArrayList(subList);
list.stream().skip(start).limit(end).collect(Collectors.toList());
List.of()
的对比特性 | subList() |
List.of() |
---|---|---|
可变性 | 是原列表的视图,可修改 | 完全不可变 |
独立性 | 依赖原列表 | 独立数据存储 |
线程安全 | 依赖原列表的实现 | 天然线程安全 |
内存占用 | 低(仅存储引用) | 可能较高(需存储数据副本) |
Copy-On-Write简称COW,其基本思想是读写分离,当想要修改这个内容的时候,把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。相当于线程安全的集合,还不需要加锁。
主要用于读多写少的并发场景。
CopyOnWriteArrayList的整个add方法操作都是在锁的保护下进行的。
也就是说add方法是线程安全的。
缺点:内存占用高(频繁复制)
写操作开销大(需复制全量数据)
数据实时性差(读操作看到的是旧数据)
线程安全集合选择:
高并发读写:优先选 ConcurrentHashMap
、ConcurrentLinkedQueue
。
读多写少:考虑 CopyOnWriteArrayList
。
ConcurrentHashMap
线程安全的核心:
JDK 1.7:分段锁降低冲突。
JDK 1.8:CAS + synchronized
锁单个桶,进一步优化并发度。
注意事项:
即使使用线程安全集合,复合操作(如 putIfAbsent
)仍需额外同步。
迭代器是弱一致性的(反映创建时的状态)。