JUC 包详解

目录

[一、什么是 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,取代 HashTableCollections.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(换数据)

线程池:七大参数+四种拒绝策略+五种状态