JUC包详解
一、什么是JUC包?
-
JUC 全称为 Java Util Concurrent,是Java标准库中专门用于支持高并发编程的工具包,提供了一系列线程安全的类、锁机制、原子操作、并发容器和线程池等工具。
-
目标:简化多线程编程,提高并发性能,避免开发者直接操作底层线程和锁的复杂性。
二、JUC包的作用
-
提高并发性能:通过无锁算法(CAS)、分段锁、线程池等技术优化资源利用。
-
简化线程管理:提供线程池(
ExecutorService
)、并发工具类(如CountDownLatch
)等,减少手动管理线程的开销。 -
保证线程安全:提供线程安全的数据结构(如
ConcurrentHashMap
、CopyOnWriteArrayList
)和原子类(如AtomicInteger
)。 -
解决复杂并发问题:支持分布式锁、条件变量、信号量等高级同步机制。
JUC包核心类详解
(一)线程安全容器
一、ConcurrentHashMap
作用:线程安全的哈希表,支持高并发读写操作。
适用场景:高并发环境下的键值存储(如缓存、计数器)。
1. 实现原理
-
JDK7:分段锁(Segment)
将整个哈希表分成多个段(Segment),每个段独立加锁,不同段的操作可并行执行。-
默认16个段,并发度由段数决定。
-
段内结构:数组 + 链表。
-
-
JDK8+:CAS + synchronized锁单个桶(Node)
-
数组 + 链表/红黑树(链表长度≥8时转红黑树)。
-
锁粒度更细,仅锁住冲突的哈希桶(Node头节点)。
-
使用CAS尝试无锁插入,失败后通过
synchronized
锁住头节点。
-
2. 源码关键点(JDK8)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化数组
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS尝试无锁插入新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
synchronized (f) { // 锁住头节点
// 处理链表或红黑树插入
}
}
}
return null;
}
3. 对比Hashtable
特性 | ConcurrentHashMap | Hashtable |
---|---|---|
锁粒度 | 分段锁或桶锁(高并发) | 全局锁(性能差) |
Null支持 | 不允许Key/Value为null | 允许null(但易引发歧义) |
扩容机制 | 分段扩容(减少阻塞) | 全表锁扩容 |
并发度 | 高 | 低 |
面试重点:
-
为什么ConcurrentHashMap不允许null值?
避免歧义(如map.get(key)
返回null时,无法区分是key不存在还是value为null)。
二、CopyOnWriteArrayList
作用:线程安全的动态数组,读操作无锁,写操作通过复制数组实现。
适用场景:读多写少(如监听器列表、配置管理)。
1. 实现原理
-
写时复制(Copy-On-Write):
每次修改操作(add、set、remove)时,复制原数组生成新数组,修改完成后替换原数组。-
读操作直接访问原数组,无需加锁。
-
写操作加锁(
ReentrantLock
),保证线程安全。
-
2. 源码关键点
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制数组
newElements[len] = e;
setArray(newElements); // 替换原数组
return true;
} finally {
lock.unlock();
}
}
3. 优缺点
-
优点:
-
读操作完全无锁,性能极高。
-
适合读多写少的场景。
-
-
缺点:
-
写操作需要复制数组,内存占用大。
-
数据一致性弱(读操作可能读到旧数据)。
-
4. 对比Vector
特性 | CopyOnWriteArrayList | Vector |
---|---|---|
锁机制 | 写时加锁,读无锁 | 所有方法加锁(性能差) |
内存开销 | 写操作复制数组,开销大 | 无额外内存开销 |
适用场景 | 读多写少 | 写操作频繁 |
面试重点:
-
为什么CopyOnWriteArrayList的迭代器不会抛出
ConcurrentModificationException
?
迭代器遍历的是原数组的快照,写操作不影响迭代过程。
(二)原子类
三、Atomic类(如AtomicInteger)
作用:通过CAS(Compare And Swap)实现无锁原子操作。
适用场景:计数器、状态标志等需要原子更新的场景。
1. 核心原理
-
CAS操作:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
-
比较当前值与期望值,若相等则更新为新值,否则重试。
-
硬件级指令(如x86的
cmpxchg
)保证原子性。
2. 解决ABA问题
-
AtomicStampedReference:通过版本号(Stamp)避免ABA问题。
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0);
atomicRef.compareAndSet(100, 101, 0, 1); // 版本号从0→1
3. 常见Atomic类
类名 | 作用 |
---|---|
AtomicInteger | 原子更新整型值 |
AtomicLong | 原子更新长整型值 |
AtomicReference | 原子更新对象引用 |
AtomicIntegerArray | 原子更新整型数组中的元素 |
4. 示例代码
AtomicInteger counter = new AtomicInteger(0);
// 多线程安全自增
counter.incrementAndGet(); // 输出:1
面试重点:
-
CAS的缺点是什么?
-
ABA问题(通过版本号解决)。
-
自旋时间长时CPU开销大(如高并发场景)。
-
总结与对比
类名 | 核心机制 | 适用场景 | 性能特点 |
---|---|---|---|
ConcurrentHashMap | CAS + 分段锁/桶锁 | 高并发读写 | 高吞吐量,低锁竞争 |
CopyOnWriteArrayList | 写时复制 + ReentrantLock | 读多写少 | 读无锁,写开销大 |
Atomic类 | CAS无锁操作 | 简单原子操作(如计数器) | 无锁,高并发下性能优异 |
面试高频问题
-
ConcurrentHashMap在JDK7和JDK8中的实现区别?
JDK7使用分段锁,JDK8改用CAS+桶锁,减少锁粒度。 -
CopyOnWriteArrayList的写操作为什么加锁?
防止多个线程同时修改导致数据不一致。 -
Atomic类能否保证多个操作的原子性?
单个CAS操作是原子的,但多个操作需结合AtomicReference
或锁。 -
如何选择ConcurrentHashMap和Hashtable?
高并发场景优先选择ConcurrentHashMap。