Java 多线程与 JUC
并发面试核心:线程状态、锁、JMM、线程池、并发容器。详解 JUC、JMM、线程池。
创建线程
| 方式 | 说明 | 生产环境 |
|---|---|---|
继承 Thread | 耦合线程与任务 | 不推荐 |
Runnable | 任务与线程分离 | 可用 |
Callable + Future | 有返回值 | 可用 |
| 线程池 | 复用、限流、队列 | 推荐 |
线程状态
NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
| 状态 | 触发 |
|---|---|
| BLOCKED | 等 synchronized 监视器 |
| WAITING | wait() / join() / LockSupport.park() |
| TIMED_WAITING | sleep(n) / wait(n) |
synchronized vs ReentrantLock
| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 层面 | JVM 关键字 | JUC API |
| 释放 | 自动 | 须 unlock()(finally) |
| 公平锁 | 非公平 | 可选公平 |
| 可中断 | 否 | lockInterruptibly |
| 多条件 | 单一 wait/notify | 多个 Condition |
| 锁升级 | 偏向→轻量→重量 | 无升级概念 |
synchronized 锁的是对象监视器(实例 this / Class),不是「代码块文字」。
volatile
| 保证 | 不保证 |
|---|---|
| 可见性(写立即对其他线程可见) | i++ 原子性 |
| 禁止部分指令重排序 | 复合逻辑互斥 |
典型用途:状态标志、双重检查锁中的 instance(配合 synchronized)。
sleep vs wait
| sleep | wait | |
|---|---|---|
| 所属 | Thread | Object |
| 释放锁 | 否 | 是 |
| 唤醒 | 超时 | notify / notifyAll |
| 使用 | 任意处 | 必须在 synchronized 内 |
线程池核心参数
new ThreadPoolExecutor(
corePoolSize, // 核心线程
maximumPoolSize, // 最大线程
keepAliveTime, // 非核心空闲存活
unit,
workQueue, // 任务队列
threadFactory,
handler // 拒绝策略
);执行顺序:核心未满 → 新建核心线程;满了 → 入队;队满 → 扩到 max;仍满 → 拒绝策略。
| 拒绝策略 | 行为 |
|---|---|
| AbortPolicy | 抛异常(默认) |
| CallerRunsPolicy | 调用者线程执行(降速) |
| DiscardPolicy | 丢弃 |
| DiscardOldestPolicy | 丢最老任务 |
生产注意:不用 Executors.newFixedThreadPool(无界队列堆积);推荐 有界队列 + 明确 max + CallerRunsPolicy。
ThreadLocal
- 每个线程独立副本,底层
Thread → ThreadLocalMap - 线程池场景:
finally中remove(),否则复用线程导致脏数据 / 内存泄漏
ConcurrentHashMap
| JDK | 机制 |
|---|---|
| 7 | Segment 分段锁 |
| 8+ | CAS 插入空桶 + synchronized 锁桶头 |
不允许 null key/value(歧义:不存在 vs 值为 null)。
AQS(了解)
ReentrantLock、Semaphore、CountDownLatch 等基于 AQS(状态 + CLH 队列)实现排队获取锁/许可。
常见面试题
Q:如何保证线程安全?
A:不可变对象、synchronized/Lock、原子类(AtomicInteger)、并发容器、ThreadLocal 隔离。
Q:死锁四个条件?
A:互斥、占有且等待、不可抢占、循环等待。线程级见 死锁;DB 见 数据库死锁。
Q:CountDownLatch vs CyclicBarrier?
A:Latch 一次性倒数;Barrier 可循环重用,线程互相等到齐。
Q:为什么用线程池而不是 new Thread?
A:降低创建销毁开销、控制最大并发、统一队列与拒绝策略。