Java 并发编程
线程 / 锁 / 线程池
这节啃 Java 后端第二个大厂考点:并发编程。从最基础的 Thread 到 synchronized 锁升级、再到 ThreadPoolExecutor 线程池——写 Java 后端绕不开。
学完这节:你能解释清楚
synchronized 锁升级过程、说出 volatile 和 synchronized 的本质区别、能正确配置一个生产级线程池、知道大厂面试爱问的"AQS、ThreadLocal、CAS"是怎么回事。
1
进程 vs 线程:为什么要并发?
💡 动机:单线程不够用,才需要多线程
| 对比 | 进程 | 线程 |
|---|---|---|
| 内存 | 独立内存空间 | ⭐ 共享进程内存 |
| 开销 | 大(创建慢、切换慢) | ⭐ 小(轻量) |
| 通信 | 进程间通信(IPC)复杂 | ⭐ 直接共享变量 |
| 类比 | 一个工厂(独立) | 工厂里的工人(协作) |
为什么需要并发?
- CPU 多核 → 多线程能榨干硬件
- I/O 阻塞(数据库 / 网络)→ 用线程切换来掩盖等待
- 接口响应时间 → 异步处理提高吞吐
2
创建线程的 3 种方式
💡 动机:先把"怎么开线程"会了,再谈线程安全
方式 1:继承 Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程在跑:" + Thread.currentThread().getName());
}
}
new MyThread().start(); // start() 才真正开线程,run() 只是普通方法
方式 2:实现 Runnable(推荐)
// 推荐:Java 单继承,Runnable 不占继承位
Runnable task = () -> System.out.println("hello from " + Thread.currentThread().getName());
new Thread(task).start();
方式 3:实现 Callable(拿返回值 / 抛异常)
// 区别:Callable 有返回值 + 能抛 checked 异常
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> ft = new FutureTask<>(task);
new Thread(ft).start();
Integer result = ft.get(); // 阻塞等结果
⭐ 必背口诀:
- 想拿返回值 → Callable + Future
- 只想执行任务 → Runnable + Thread
- 生产环境别直接 new Thread,用线程池(下文)
3
线程 6 种状态
💡 动机:看线程 dump / 排查卡死时必看状态
// Thread.State 枚举的 6 个值
public enum State {
NEW, // 刚 new 完,没 start
RUNNABLE, // 正在 JVM 里跑(CPU 上 or 等 CPU)
BLOCKED, // 阻塞在 synchronized 锁
WAITING, // 无限等待(Object.wait / Thread.join)
TIMED_WAITING, // 限时等待(sleep / wait(timeout))
TERMINATED // 跑完了
状态转换图(⭐ 面试常画)
new Thread() start()
│ │
▼ ▼
NEW ──────────────────▶ RUNNABLE ◀──┐
│ │ CPU 调度
┌──────────┤ │
│ ▼ │
┌─────────────┘ 拿到锁 │
│ sleep/join/wait │
▼ │
TIMED_WAITING / WAITING ─────────────┘
│ 超时 / notify
▼
BLOCKED(抢锁失败)
│ 拿到锁
▼
RUNNABLE ──── run() 结束 ──▶ TERMINATED
4
synchronized + 锁升级 ⭐⭐ 面试必问
💡 动机:锁是 Java 后端最容易出 bug 的地方,也是面试官最爱问的
3 种用法
// 1. 修饰实例方法(锁 = this 对象)
public synchronized void save() { ... }
// 2. 修饰静态方法(锁 = Class 对象)
public static synchronized void create() { ... }
// 3. 修饰代码块(锁 = 括号里的对象)
public void update() {
synchronized (this) { // 等价于方法 1
...
}
synchronized (SomeClass.class) { // 等价于方法 2
...
}
}
⭐⭐ 锁升级(Java 6+ 优化)
JDK 6 之前 synchronized = 直接 OS 互斥锁,很重。JDK 6+ 优化成"渐进式升级":
| 锁级别 | 触发条件 | 开销 |
|---|---|---|
| ⭐ 偏向锁 | ⭐ 只有 1 个线程访问 | ⭐ 几乎无开销(CAS) |
| ⭐ 轻量级锁 | 多线程但不冲突 | 小(自旋 CAS,不阻塞) |
| ⭐ 重量级锁 | 多线程真冲突 | 大(OS 互斥、线程阻塞) |
⭐ 升级路径:
- 对象头记下第一个访问它的线程 ID(偏向)
- 第二个线程来抢 → 撤销偏向锁,升级为轻量级,两个线程 CAS 抢
- CAS 抢失败 / 自旋超阈值 → 升级为重量级,OS mutex,未抢到的线程挂起
⭐ 必背的 3 个注意点
- 锁的对象要一致:
synchronized (this)和synchronized (obj)是两把不同的锁 - 不要锁 String / Integer:它们在常量池可能被复用,导致"逻辑上不同的对象锁同一把锁"
- 锁内不要调用外部代码:外部代码可能慢 / 死锁 / 抛异常
5
volatile + JMM(Java 内存模型)⭐⭐
💡 动机:每个 Java 工程师都会背"synchronized 四大特性",但不知道为啥
JMM 三大问题
| 问题 | 原因 | volatile | synchronized |
|---|---|---|---|
| ⭐ 可见性 | CPU 缓存:线程 A 改了,线程 B 看不见 | ✅ 解决 | ✅ 解决 |
| ⭐ 原子性 | 多步操作被切走(如 i++) | ❌ 不解决 | ✅ 解决 |
| ⭐ 有序性 | JIT 重排序:编译器/CPU 优化打乱顺序 | ✅ 部分解决 | ✅ 解决 |
⭐ 经典口诀(synchronized 三大特性):
原子 + 可见 + 有序,一个锁解决三个问题,但代价是"性能"。
volatile 只能保"可见 + 有序",不能保"原子",所以不能替代锁。
// 经典用法:双重检查锁单例
public class Singleton {
private static volatile Singleton instance; // volatile 防止指令重排
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁,快)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(防止并发)
instance = new Singleton(); // new 实际分 3 步:
// 1. 分配内存
// 2. 初始化对象
// 3. instance 指向内存
// volatile 防止 1→3→2 的重排
}
}
}
return instance;
}
}
6
线程池 ThreadPoolExecutor ⭐⭐
💡 动机:生产代码 99% 用线程池,绝不能直接 new Thread
线程池的好处:复用线程、控制并发数、任务排队、统一异常处理。
7 大参数(必背)
new ThreadPoolExecutor(
int corePoolSize, // 核心线程数(即使空闲也不回收)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程创建工厂
RejectedExecutionHandler handler // 拒绝策略
);
⭐ 任务执行流程
// 提交任务 → 流程:
// 1. 线程数 < corePoolSize → 创建新线程执行
// 2. 线程数 ≥ corePoolSize → 任务进 workQueue
// 3. 队列满了 → 创建新线程(直到 maximumPoolSize)
// 4. 线程数 ≥ maximumPoolSize → 触发拒绝策略
⭐ 4 种拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy | ⭐ 默认,抛 RejectedExecutionException |
CallerRunsPolicy | ⭐⭐ 由调用者线程执行(最推荐) |
DiscardPolicy | 直接丢弃(不抛异常) |
DiscardOldestPolicy | 丢最老的任务,再尝试提交 |
⭐ 生产推荐配置:
// CPU 密集型(计算)
int threads = Runtime.getRuntime().availableProcessors() + 1;
// I/O 密集型(数据库、网络)
int threads = Runtime.getRuntime().availableProcessors() * 2;
// 推荐:Executors.newFixedThreadPool(N) 固定大小
// 或 ThreadPoolExecutor 自定义(更可控)
7
JUC 工具类 + 常见面试题
💡 动机:JDK 5+ 加了 java.util.concurrent 包,并发开发已经不用裸 synchronized 了
| 工具类 | 作用 | 典型场景 |
|---|---|---|
ConcurrentHashMap |
⭐⭐ 线程安全的 Map(分段锁 / CAS) | 替代 Hashtable、Collections.synchronizedMap |
CountDownLatch |
⭐ 等 N 个线程都完成 | 主线程等所有子线程跑完 |
CyclicBarrier |
⭐ N 个线程互相等,到齐一起跑 | 多线程分阶段计算 |
Semaphore |
⭐ 控制同时只有 N 个线程进 | 限流 |
ThreadLocal |
⭐ 每个线程自己的变量副本 | 事务、用户上下文 |
AtomicInteger |
⭐ CAS 实现的原子计数器 | 替代 volatile int + synchronized |
⭐ 经典面试题(背下来):
synchronized和ReentrantLock区别?→ ReentrantLock 更灵活(可中断 / 公平锁 / tryLock),synchronized 是关键字、自动释放ConcurrentHashMap怎么保证线程安全?→ JDK 7 分段锁,JDK 8+ 改成 CAS + synchronized 单桶锁ThreadLocal内存泄漏怎么办?→ 用完 必须 remove(),特别是线程池场景wait和sleep区别?→ wait 释放锁,sleep 不释放;wait 必须在 synchronized 里- 为什么
wait必须在 synchronized 块里?→ 防止"伪唤醒"导致的竞态条件(著名的"lost wakeup"问题)
你的作业(分步走)
- 画线程状态图:凭记忆画"NEW → RUNNABLE → BLOCKED → TERMINATED"的状态转换图,标注每个状态会因什么方法触发
- 3 种创建方式:分别用 Thread / Runnable / Callable 写 3 个版本,启动 3 个线程,Callable 那个要拿返回值
- 复现竞态条件:写个 100 个线程同时
count++的程序(不用锁),看最终 count 不是 100;加锁后再跑对比 - volatile 验证可见性:写代码用 volatile 修饰 boolean flag,不开 volatile 时子线程可能死循环,开了之后能正常停止
- 线程池配置:手写
ThreadPoolExecutor(不用 Executors),配 core=2, max=5, queue=10, 拒绝策略 CallerRunsPolicy,跑 20 个任务看输出 - 面试题答题:凭记忆答 3 道:① volatile 能替代锁吗 ② wait 和 sleep 区别 ③ ConcurrentHashMap JDK 8 怎么保证线程安全
- CountDownLatch 实战:主线程启动 5 个子线程做"并行计算",用 CountDownLatch 等全部完成再汇总结果