前言
并发编程是 Java 后端面试的"必考题"。这篇文章从 线程 和 锁 两个核心维度出发,涵盖实习生面试的高频考点,并适度深入源码帮助理解。
一、线程基础
1.1 创建线程的三种方式
// 方式一:继承 Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
// 方式二:实现 Runnable(推荐)
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task running");
}
}
// 方式三:实现 Callable + FutureTask(带返回值)
class MyCallable implements Callable<String> {
@Override
public String call() {
return "Result";
}
}
面试点: Runnable 比 Thread 好在哪?
- Java 单继承,实现接口不影响继承其它类
- 解耦任务定义与执行方式
- 方便线程池复用
1.2 线程生命周期
NEW ──start()──→ RUNNABLE ──获取锁──→ BLOCKED
│ │
等待CPU调度 释放锁后回到RUNNABLE
│
得到时间片 → RUNNING
│
┌───────┼───────┐
↓ ↓ ↓
WAITING TIMED_WAITING TERMINATED
(wait/join) (sleep(ms))
所有状态在 Thread.State 枚举中定义(JDK 源码):
// 来自 OpenJDK Thread.java
public enum State {
NEW, // 新建,尚未start
RUNNABLE, // 可运行(就绪+运行合并)
BLOCKED, // 阻塞等待monitor锁
WAITING, // 等待(Object.wait, Thread.join, LockSupport.park)
TIMED_WAITING, // 带超时的等待
TERMINATED // 已结束
}
二、synchronized 关键字
2.1 用法
// 1. 修饰实例方法 → 锁的是 this
public synchronized void method() { }
// 2. 修饰静态方法 → 锁的是 Class 对象
public static synchronized void staticMethod() { }
// 3. 同步代码块 → 锁指定对象
public void block() {
synchronized (this) {
// 临界区
}
}
2.2 底层原理(重点)
synchronized 在 JVM 中是通过 Monitor(监视器锁)实现的。
看字节码:
public void method() {
synchronized (this) {
System.out.println("hello");
}
}
反编译看关键字节码指令:
monitorenter // 进入,尝试获取锁
...
monitorexit // 退出,释放锁(正常路径)
monitorexit // 退出,释放锁(异常路径,保证一定会释放)
JDK 6 之后的大优化: 锁不再是"重量级"的。JVM 引入了锁升级机制。
2.3 锁升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)
无锁 → 偏向锁 → 轻量级锁(自旋锁)→ 重量级锁
←——————————————————————————————→ 只能升级,不能降级
| 阶段 | 触发条件 | 开销 |
|---|---|---|
| 偏向锁 | 只有一个线程访问同步块 | 极低,只在CAS一次 |
| 轻量级锁 | 少量线程交替获取,不竞争 | 自旋等待,不挂起 |
| 重量级锁 | 多线程真正竞争 | 线程挂起+唤醒,上下文切换 |
面试高频题:“synchronized 是重量级锁吗?”
- 答:JDK 6 之后不再是。默认是偏向锁,只有真正多线程竞争时才会升级为重量级锁。
三、Lock 接口与实现
3.1 ReentrantLock
Lock lock = new ReentrantLock(true); // fair = true 表示公平锁
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 一定在finally中释放!
}
3.2 synchronized vs Lock
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取/释放 | 自动 | 手动(必须finally释放) |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 超时尝试 | ❌ | ✅ tryLock(timeout) |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait/notify | Condition(更灵活) |
| 性能 | 优化后≈Lock | 差不多 |
3.3 AQS(AbstractQueuedSynchronizer)源码级理解
ReentrantLock 的核心是 AQS,这是 Java 并发包的基石。
// AQS 核心字段(简化理解)
public abstract class AbstractQueuedSynchronizer {
// 核心:volatile 的 state
// ReentrantLock 中,0=无锁,>0=持有锁(可重入计数)
private volatile int state;
// CLH 队列头尾(双向链表,存放等待线程)
private transient Node head;
private transient Node tail;
// 独占模式:当前持有锁的线程
private transient Thread exclusiveOwnerThread;
}
加锁流程(非公平锁):
lock()
├─ CAS尝试设置 state=1(快速抢锁)
│ ├─ 成功 → 设置 exclusiveOwnerThread = 当前线程
│ └─ 失败 → acquire(1)
│ ├─ tryAcquire:再次尝试(可能重入)
│ └─ 失败 → addWaiter 加入CLH队列 → acquireQueued 阻塞
└─ 公平锁跳过CAS抢锁,直接acquire
CLH 队列 是一个双向链表。每个 Node 持有 waitStatus(是否被取消、是否需要唤醒后继等)。
四、volatile 关键字
4.1 两大语义
private volatile boolean flag = true;
- 可见性:一个线程修改 flag,其它线程立即可见(绕过 CPU 缓存,直接读写主存)
- 禁止指令重排序:读写 volatile 变量前后的指令不能被重排序
4.2 经典例子:DCL 单例
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
为什么需要 volatile? instance = new Singleton() 不是原子操作:
分配内存 → 初始化对象 → 赋值给引用
① ② ③
JIT 可能重排序为 ①→③→②,另一个线程读到非 null 但未初始化的对象。volatile 禁止这个重排序。
五、常见并发工具
5.1 CountDownLatch
// 等待 N 个线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 工作...
latch.countDown(); // 计数减1
}).start();
}
latch.await(); // 主线程等待计数到0
5.2 Semaphore(信号量)
Semaphore sem = new Semaphore(3); // 同时允许3个线程
sem.acquire(); // 获取许可,没有则阻塞
// 访问资源
sem.release(); // 释放许可
5.3 面试题:三个线程交替打印ABC
private int state = 0;
private final Lock lock = new ReentrantLock();
private final Condition[] conditions = {
lock.newCondition(),
lock.newCondition(),
lock.newCondition()
};
public void print(int id) {
for (int i = 0; i < 10; i++) {
lock.lock();
try {
while (state % 3 != id) {
conditions[id].await();
}
System.out.print((char) ('A' + id));
state++;
conditions[(id + 1) % 3].signal();
} finally {
lock.unlock();
}
}
}
六、常见面试题汇总
| 题目 | 回答要点 |
|---|---|
| 线程和进程的区别? | 进程=资源分配最小单位,线程=CPU调度最小单位,共享堆空间 |
| sleep 和 wait 的区别? | sleep不释放锁,wait释放锁;sleep需要catch InterruptedException,wait需在synchronized块内 |
| 死锁的条件? | 互斥、持有并等待、不可剥夺、循环等待 |
| 怎么避免死锁? | 按固定顺序加锁、使用 tryLock 超时、减少锁粒度 |
| ThreadLocal 了解吗? | 每个线程维护自己的副本,用 Entry(WeakReference) 存储,注意内存泄漏 |
| 线程池参数? | corePoolSize, maxPoolSize, keepAliveTime, workQueue, threadFactory, handler |
七、线程池
7.1 ThreadPoolExecutor 核心参数
new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 阻塞队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
执行流程:
提交任务
├─ 线程数 < corePoolSize → 创建新线程执行
├─ 线程数 >= corePoolSize → 放入阻塞队列
├─ 队列满 → 线程数 < maxPoolSize → 创建临时线程
└─ 队列满 & 线程数 >= maxPoolSize → 执行拒绝策略
7.2 拒绝策略
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException |
| CallerRunsPolicy | 提交任务的线程自己执行 |
| DiscardPolicy | 直接丢弃 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务 |
总结
并发编程的核心脉络:
- 线程:创建、状态、中断
- 共享:synchronized、volatile、Lock
- 协作:wait/notify、Condition、LockSupport
- 工具:CountDownLatch、Semaphore、CyclicBarrier
- 容器:ConcurrentHashMap、CopyOnWriteArrayList
- 线程池:ThreadPoolExecutor、参数调优
面试实习岗位,掌握前三个就足以应对大多数问题了。建议结合源码(AQS 加锁流程、synchronized 锁升级)深入理解而不是死记硬背。