目录
[一、什么是 JUC?](#一、什么是 JUC?)
[2.1 ConcurrentHashMap](#2.1 ConcurrentHashMap)
[2.2 CopyOnWriteArrayList](#2.2 CopyOnWriteArrayList)
[2.3 BlockingQueue(阻塞队列)](#2.3 BlockingQueue(阻塞队列))
[3.1 Lock 接口 vs synchronized](#3.1 Lock 接口 vs synchronized)
[3.2 ReentrantLock(可重入锁)](#3.2 ReentrantLock(可重入锁))
[3.3 ReentrantReadWriteLock(读写锁)](#3.3 ReentrantReadWriteLock(读写锁))
[3.4 StampedLock(Java 8 引入)](#3.4 StampedLock(Java 8 引入))
[4.1 CountDownLatch(倒计时器)](#4.1 CountDownLatch(倒计时器))
[4.2 CyclicBarrier(循环栅栏)](#4.2 CyclicBarrier(循环栅栏))
[4.3 Semaphore(信号量)](#4.3 Semaphore(信号量))
[4.4 Exchanger(交换器)](#4.4 Exchanger(交换器))
[5.1 为什么要用线程池?](#5.1 为什么要用线程池?)
[5.2 ThreadPoolExecutor 七大参数](#5.2 ThreadPoolExecutor 七大参数)
[5.3 线程池的五种状态](#5.3 线程池的五种状态)
[5.4 Executors 快捷创建线程池(不推荐)](#5.4 Executors 快捷创建线程池(不推荐))
[5.5 线程池大小如何设置?](#5.5 线程池大小如何设置?)
[6.1 基础题](#6.1 基础题)
[6.2 线程池专项](#6.2 线程池专项)
[6.3 同步工具专项](#6.3 同步工具专项)
[6.4 进阶题](#6.4 进阶题)
[6.5 代码题](#6.5 代码题)
一、什么是 JUC?
JUC = java.util.concurrent 包的简称,Java 5 引入的标准并发工具包。
面试话术:JUC 是 Java 提供的一套开箱即用的并发编程工具,覆盖了并发集合、锁、线程同步工具和线程池四大领域,让我们不用自己造轮子就能写出线程安全的代码。
二、并发集合(线程安全的容器)
2.1 ConcurrentHashMap
是什么 :线程安全的 HashMap,取代 HashTable 和 Collections.synchronizedMap()。
演进:
-
Java 7:分段锁(Segment,继承 ReentrantLock),把 Map 分成 16 段,锁粒度是段级别
-
Java 8:CAS + synchronized,锁粒度降到桶级别,并发度更高,且读操作基本无锁
核心方法:
// 没有才放,避免并发覆盖
map.putIfAbsent(key, value);
// 计算并放值,原子操作
map.computeIfAbsent(key, k -> createValue(k));
// 安全遍历,不会抛 ConcurrentModificationException
for (Map.Entry<K, V> entry : map.entrySet()) { }
面试话术:ConcurrentHashMap 做了一层封装,读操作不加锁,写操作锁粒度很细(Java 8 是桶级),所以高并发场景下用它替代 HashTable 和 synchronizedMap,性能能差十几倍。
2.2 CopyOnWriteArrayList
是什么:线程安全的 ArrayList。
原理:写时复制------每次修改(add/set/remove)都会复制一份新数组,写完之后让内部引用指向新数组。
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
面试话术:CopyOnWriteArrayList 适用于读多写少的场景,比如黑名单、监听器列表。因为读操作完全不加锁,写操作要复制数组,所以写频繁的话内存开销大。
2.3 BlockingQueue(阻塞队列)
核心接口,实现类:
| 实现类 | 特点 |
|---|---|
ArrayBlockingQueue |
有界,数组实现,公平锁可选 |
LinkedBlockingQueue |
可选有界/无界,链表实现 |
PriorityBlockingQueue |
带优先级,无界 |
SynchronousQueue |
容量为 0,直接交付 |
DelayQueue |
延迟队列,到期才能取 |
LinkedTransferQueue |
有 transfer 语义 |
核心方法(4 种行为):
| 抛异常 | 返回布尔 | 阻塞等待 | 超时等待 | |
|---|---|---|---|---|
| 入队 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
| 出队 | remove() |
poll() |
take() |
poll(time, unit) |
面试话术:阻塞队列的核心是 put 和 take,队列满时 put 会阻塞,队列空时 take 会阻塞。最经典的应用就是线程池的任务队列,生产者(提交任务)和消费者(工作线程)之间解耦。
三、锁
3.1 Lock 接口 vs synchronized
| synchronized | Lock | |
|---|---|---|
| 用法 | 关键字,自动获取释放 | 手动 lock() / unlock() |
| 灵活性 | 低,只能同步块/方法 | 高,可跨方法 |
| 尝试获取 | 不行,获取不到就阻塞 | tryLock() 可尝试 |
| 中断响应 | 不支持 | lockInterruptibly() |
| 公平性 | 非公平 | 可配置公平/非公平 |
| 条件等待 | wait() / notify() |
Condition.await() / signal() |
java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 业务代码
} finally {
lock.unlock(); // 必须手动释放
}
面试话术:synchronized 是 Java 关键字,自动加解锁,适合简单场景。Lock 是接口,提供了 tryLock 尝试获取、lockInterruptibly 可中断等待、公平锁等更灵活的特性,但必须手动 unlock,通常放在 finally 块里。
3.2 ReentrantLock(可重入锁)
可重入:同一个线程可以多次获取同一把锁,不会死锁自己。
示例:
java
ReentrantLock lock = new ReentrantLock(true); // true = 公平锁
lock.lock();
// 在这个锁保护下又可以调另一个也需要这个锁的方法
lock.lock();
lock.unlock();
lock.unlock(); // 加了几次就释放几次
公平 vs 非公平:
-
非公平(默认):新线程来了会插队,性能高但可能线程饿死
-
公平:FIFO 排队,代价是吞吐量下降
面试话术:ReentrantLock 和 synchronized 都是可重入的。区别在于 ReentrantLock 可以指定公平锁,还支持 tryLock 带超时等待、lockInterruptibly 响应中断。Java 6 之后 synchronized 做了大量优化(锁升级、偏向锁),简单场景两者性能差别不大。
3.3 ReentrantReadWriteLock(读写锁)
java
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock(); // 读锁:多个线程可以同时读
rw.writeLock().lock(); // 写锁:只有一个线程能写,且写的时候不能读
规则:读读不互斥,读写互斥,写写互斥。
面试话术:读写锁适合读多写少的场景,读锁可以被多个线程共享,写锁是独占的。如果有大量并发读,读写锁的吞吐量比普通锁高很多。
3.4 StampedLock(Java 8 引入)
读写锁的进一步优化,支持乐观读。
java
StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 乐观读:不加锁,先读
// 读取数据...
if (!sl.validate(stamp)) { // 检查读数期间有没有被写
stamp = sl.readLock(); // 被写了,升级为悲观读
try {
// 重新读
} finally {
sl.unlockRead(stamp);
}
}
面试话术:StampedLock 比 ReadWriteLock 多了一个乐观读模式,读操作先不加锁直接读,读完之后验证一下有没有被写线程改过,没改过就直接用,这能让读操作完全无锁,性能更高。
四、同步工具类
4.1 CountDownLatch(倒计时器)
让一个线程等待其他 N 个线程完成后再继续。
java
CountDownLatch latch = new CountDownLatch(3); // 倒计时 3 次
// 线程 1/2/3 分别执行,每个最后调 latch.countDown();
latch.countDown(); // 计数减 1
// 主线程等待所有任务完成
latch.await(); // 阻塞直到计数为 0
System.out.println("三个线程都干完了");
特点 :一次性,计数归零后不能再重置。
面试话术:CountDownLatch 像一个门闩,计数器是 3,就等 3 次 countDown 之后门闩打开。最经典场景是主线程等 N 个子线程都初始化完成后再继续。注意它只能用一次。
4.2 CyclicBarrier(循环栅栏)
N 个线程互相等待,都到齐了再一起往下走。
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("三个人都到了,出发!");
});
// 每个线程:
barrier.await(); // 等待其他人,人数到了就执行回调,然后放行
特点 :可循环使用,所有线程到达后自动重置。
面试话术:CyclicBarrier 像旅游团的集合点,导游说"人齐了就走",所有人到齐了才能出发。比 CountDownLatch 多了一个可循环和回调功能。CountDownLatch 更像发令枪,一个线程等其他人到齐了就开跑,其他线程不用继续等。
4.3 Semaphore(信号量)
控制同时访问某个资源的线程数量。
java
Semaphore semaphore = new Semaphore(3); // 最多 3 个线程同时访问
semaphore.acquire(); // 获取许可证,没有就阻塞
// 访问资源...
semaphore.release(); // 释放许可证
面试话术:Semaphore 像停车场的道闸,车位只有 3 个,来一辆车放一辆,满了就排队等。用在限流、连接池、数据库连接控制等场景。
4.4 Exchanger(交换器)
两个线程交换数据。
java
Exchanger<String> exchanger = new Exchanger<>();
// 线程 A:
String dataA = exchanger.exchange("A的数据");
// 此时 dataA 拿到了 B 的数据
面试话术:Exchanger 是线程间的数据交换点,两个线程在同一个交换点交替换数据。用在遗传算法、校对工作等双方互换信息的场景。
五、线程池
5.1 为什么要用线程池?
| 不用线程池 | 用线程池 |
|---|---|
| 来一个任务 new 一个线程 | 复用已创建好的线程 |
| 线程无限创建,OOM 风险 | 控制最大并发数 |
| 销毁线程开销大 | 线程存活管理 |
| 没法管理、监控 | 统一管理 |
5.2 ThreadPoolExecutor 七大参数
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
执行流程(重要!):
任务提交
│
▼
核心线程数满了?
├── 没满 → 创建核心线程执行
└── 满了 → 进入任务队列
│
▼
队列满了?
├── 没满 → 排队等待
└── 满了 → 创建非核心线程
│
▼
线程数达到 maximumPoolSize?
├── 没到 → 创建临时线程执行
└── 到了 → 执行拒绝策略
拒绝策略(4 种):
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) |
抛 RejectedExecutionException |
CallerRunsPolicy |
让提交任务的线程自己执行 |
DiscardPolicy |
静默丢弃 |
DiscardOldestPolicy |
丢弃队列中最旧的任务,再尝试提交 |
5.3 线程池的五种状态
RUNNING → SHUTDOWN(不接受新任务,处理完队列任务)
RUNNING → STOP(不接受新任务,不处理队列,中断正在执行的)
RUNNING/SHUTDOWN/STOP → TIDYING(任务都执行完了)
TIDYING → TERMINATED(terminated() 钩子执行完)
面试话术:线程池状态控制比线程优先级高,比如 SHUTDOWN 状态下即使有空闲核心线程也不会执行新任务。shutdown() 和 shutdownNow() 的区别:前者不处理完队列不收兵,后者暴力中断不等人。
5.4 Executors 快捷创建线程池(不推荐)
java
Executors.newFixedThreadPool(3); // 固定线程数,队列 Integer.MAX_VALUE → 可能 OOM
Executors.newCachedThreadPool(); // 无限创建线程 → 可能 OOM
Executors.newSingleThreadExecutor();// 单线程
Executors.newScheduledThreadPool(3);// 定时任务
面试话术:阿里巴巴规范手册明确禁止使用 Executors 创建线程池,因为它的默认参数有问题------newFixedThreadPool 用了无界队列(Integer.MAX_VALUE),任务积压可能 OOM;newCachedThreadPool 最大线程无限,请求量大也 OOM。推荐自己 new ThreadPoolExecutor 显式传参。
5.5 线程池大小如何设置?
CPU 密集型 :N + 1(N = CPU 核数) IO 密集型 :2N 或更大(取决于 IO 等待时间占比)
java
// 获取 CPU 核数
int cpuCores = Runtime.getRuntime().availableProcessors();
面试话术:CPU 密集型任务线程数不宜超过 CPU 核数太多,否则线程切换反而降低性能。IO 密集型任务大量时间在等磁盘/网络响应,CPU 是空闲的,可以多开线程把等待时间利用起来。
六、常见面试题与回答话术
6.1 基础题
Q1:synchronized 和 ReentrantLock 的区别?
话术:synchronized 是关键字,自动加解锁,支持锁升级(偏向→轻量→重量)。ReentrantLock 是 API 级别的,需要手动 unlock,但提供了 tryLock、lockInterruptibly、公平锁等更灵活的功能。Java 6 之后 synchronized 做了大量优化,简单场景两者性能差异不大。核心区别是 synchronized 自动省心,ReentrantLock 灵活可控。
Q2:ConcurrentHashMap 的底层原理?
话术:Java 8 的实现是数组+链表+红黑树,用了 CAS + synchronized 保证线程安全。put 时先对 key 做 hash 定位到桶,如果桶为空就用 CAS 写入;如果有冲突就锁住桶头节点,遍历链表/红黑树。读操作完全不加锁,靠 volatile 保证可见性。和 HashTable 比,HashTable 是方法级锁,并发度极低,ConcurrentHashMap 的并发度是桶级别的。
Q3:volatile 关键字能保证什么?
话术:volatile 保证两件事:① 可见性------当一个线程修改了 volatile 变量,其他线程立即可见,不会读到自己线程的工作内存副本;② 禁止指令重排序------防止 JVM 优化时把代码顺序调乱。但 volatile 不保证原子性,比如 count++ 这种操作 volatile 也扛不住。
Q4:ThreadLocal 是什么?有什么坑?
话术:ThreadLocal 是线程本地变量,每个线程都有自己的独立副本,互不干扰。经典应用是 Spring 的事务管理(把数据库连接放到 ThreadLocal 里)。最大坑是内存泄漏------ThreadLocalMap 的 key 是弱引用,GC 后 key 变 null,但 value 是强引用还在,如果不手动 remove,整个线程池的线程一直活着就永远释放不了。所以每次用完后一定要调 remove()。
Q5:什么是 CAS?有什么问题?
话术:CAS(Compare And Swap)是 CPU 原语指令,比较并交换,分三步:① 读内存值 V;② 和预期值 A 比较;③ 如果相等就改成目标值 B,不相等就重试。优点是没用锁,性能好。有三个问题:① ABA 问题(加版本号解决,AtomicStampedReference);② 自旋时间太长 CPU 开销大;③ 只能保证单个共享变量的原子操作。
6.2 线程池专项
Q6:线程池的拒绝策略有哪些?什么时候会触发?
话术:四种策略------AbortPolicy 抛异常(默认)、CallerRunsPolicy 主线程自己跑、DiscardPolicy 静默丢、DiscardOldestPolicy 丢掉最旧任务再试。触发时机是:核心线程满了 → 队列满了 → 最大线程也满了 → 新任务触发拒绝策略。
Q7:submit() 和 execute() 的区别?
话术:execute 提交 Runnable,没有返回值,抛异常直接打印堆栈。submit 提交 Callable 或 Runnable,返回 Future,可以通过 future.get() 拿结果或捕获异常。submit 底层调的还是 execute。
Q8:如何合理设置线程池参数?
话术:CPU 密集型设 N+1,IO 密集型设 2N 或更大。但实际生产中要看具体的业务指标:任务平均耗时、QPS、可接受的响应时间,通过压测调参。比如接口 TPS=200,每个任务耗时 50ms,那理论上 10 个线程就够了。还可以用动态线程池(美团开源方案),运行时改参数。
6.3 同步工具专项
Q9:CountDownLatch 和 CyclicBarrier 的区别?
话术:CountDownLatch 是倒计时器,一个线程等 N 个线程完事,只能用一次。CyclicBarrier 是栅栏,N 个线程互相等,人到齐了就放行,且可以循环使用。打个比方:CountDownLatch 像是裁判的发令枪------所有运动员就位,裁判扣扳机;CyclicBarrier 像是跟团旅游------人齐了才发车,下一站继续等人。
Q10:Semaphore 的应用场景?
话术:Semaphore 做限流最合适,比如限制接口的并发访问数为 50,超过的排队或直接返回失败。也可以用来实现连接池,acquire 时拿连接,release 时归还。
6.4 进阶题
Q11:AQS 是什么?
话术:AQS(AbstractQueuedSynchronizer)是 JUC 的基石,ReentrantLock、CountDownLatch、Semaphore 等同步器都基于它实现。核心是一个 state(volatile int)和一个 CLH 双向队列(等待线程队列)。子类通过重写 tryAcquire/tryRelease 等方法控制 state 的获取和释放,AQS 负责线程排队、阻塞、唤醒等底层机制。模板方法设计模式。
Q12:线程池的理想线程数和 CPU 核数的关系?
话术:公式是
线程数 = CPU核数 × (1 + 等待时间/计算时间)。如果任务全是 CPU 计算,等待时间≈0,线程数≈CPU核数;如果任务大量时间在等 IO,等待时间远大于计算时间,线程数就可以比 CPU 核数大很多。最终还是要通过压测验证。
Q13:如何排查线程池问题?
话术:给线程池自定义名称(通过 ThreadFactory),这样出问题能快速定位是哪个池。监控核心指标:活跃线程数、队列积压量、拒绝次数、任务执行耗时。可以用 ThreadPoolExecutor 的 beforeExecute/afterExecute 钩子方法做埋点,也可以对接 Prometheus + Grafana。
6.5 代码题
Q14:手写一个线程池(简要版)?
面试官要的不是把 ThreadPoolExecutor 背一遍,而是展示理解了核心设计。
话术:核心是生产者-消费者模式。一个阻塞队列存放任务,一组工作线程从队列里取任务执行。需要一个 volatile 变量控制线程存活。支持 corePoolSize、maxPoolSize、keepAliveTime、workQueue。关键设计点:工作线程在任务队列为空时阻塞等待(take),避免空转;新任务先看核心线程池是否满,再看队列是否满,最后判断最大线程数。
Q15:如何实现一个延迟执行的任务?
话术:用 ScheduledThreadPoolExecutor 或 DelayQueue。DelayQueue 的元素需要实现 Delayed 接口的 getDelay 方法,队列内部按延迟时间排序。也可以用定时任务框架(XXL-Job、Elastic-Job)做分布式延迟任务。
七、一句话快速记忆
JUC 四大块:并发集合、锁、同步器、线程池
三个集合:ConcurrentHashMap(高频)、CopyOnWriteArrayList(读多写少)、BlockingQueue(生产者消费者)
三种锁:ReentrantLock(灵活)、ReentrantReadWriteLock(读写分离)、StampedLock(乐观读)
四个同步器:CountDownLatch(等完事)、CyclicBarrier(等人齐)、Semaphore(限流)、Exchanger(换数据)
线程池:七大参数+四种拒绝策略+五种状态