一 基础概念:进程、线程、并发、并行
概念
| 概念 | 本质 | 记忆点 |
|---|---|---|
| 进程 | 程序的一次执行实例,资源分配的基本单位 | 有独立内存空间 |
| 线程 | CPU 调度的基本单位,进程内的执行路径 | 一个进程可含多个线程 |
| 并发(concurrent) | 单核下时间片轮转,宏观同时、微观交替 | 交替执行 |
| 并行(parallel) | 多核下真正同时执行 | 同时执行 |
| 上下文切换 | 保存/恢复程序计数器、栈帧等现场 | 代价高,是"线程越多越慢"的根因 |
线程共享 vs 私有:
- 共享:堆、方法区(元空间)
- 私有:程序计数器、虚拟机栈、本地方法栈
这条决定了局部变量天然线程安全 (栈封闭),而成员变量/静态变量才有线程安全问题。
三个性能事实
- 只有多核才有意义 :讲义实测,双核 CPU 下 4 线程并行求和比单线程快约一倍 (0.020 s/op vs 0.043 s/op);单核下两者几乎一致(0.061 vs 0.064)------单核多线程只是徒增上下文切换。
- 线程数不是越多越好:超过 CPU 核数后,上下文切换开销开始反噬吞吐量。
- 栈与栈帧 :每个线程有独立栈,方法调用压栈帧、结束出栈;
-Xss调栈大小,过小会StackOverflowError。
二 线程基础:创建、API、状态流转
四种创建方式
| 方式 | 特点 | 备注 |
|---|---|---|
继承 Thread 重写 run() |
最简单 | 单继承限制,不推荐 |
实现 Runnable + new Thread(r) |
推荐:任务与线程解耦、可共享任务对象 | 带 @FunctionalInterface,可用 lambda 简化 |
Callable + FutureTask |
有返回值、可抛受检异常 | future.get() 阻塞获取 |
线程池 execute/submit |
生产唯一推荐 | 复用线程,见第八章 |
java
// Callable + FutureTask
FutureTask<Integer> task = new FutureTask<>(() -> { return 1; });
new Thread(task, "t1").start();
Integer r = task.get(); // 阻塞等待结果,内部即"保护性暂停"模式
高频 API 语义
| 方法 | 本质 | 易错点 |
|---|---|---|
start() |
新建线程并异步执行 run() |
一个 Thread 对象只能 start 一次 ,重复抛 IllegalThreadStateException |
run() |
普通方法调用 | 直接调 run() 不会开新线程 |
sleep(ms) |
当前线程休眠 → TIMED_WAITING |
不释放锁 ;是 Thread 静态方法;sleep 写在哪个线程里就休眠哪个线程 |
yield() |
让出 CPU → 就绪态 | 只是"提示",任务调度器可完全无视;与 sleep 区别见下 |
join() / join(n) |
等待目标线程结束 | 底层是 wait/notify(保护性暂停);join(n) 取**"实际等待时间"与"设定时间"的较小值** |
interrupt() |
设置打断标记 | 打断 sleep/wait/join 会抛 InterruptedException 并清空标记 |
isInterrupted() |
读标记,不清除 | vs Thread.interrupted()(静态方法,会清除) |
setPriority(1~10) |
默认 5,数字越大优先级越高 | 只影响调度概率,调度器权力大于优先级 |
setDaemon(true) |
守护线程 | JVM 退出不管它,其 finally 不保证执行 |
yield vs sleep 精确对比:
yield : 无参数,立即执行;Running → Runnable(就绪);调度器仍可立刻再选中它
sleep : 需时间参数;Running → Timed Waiting(阻塞);调度器不会给阻塞态分配时间片
打断标记的三条铁律:
- 打断运行中 的线程 → 标记置为
true,线程继续跑,由它自己决定退出("体面地打断")。 - 打断
sleep/wait/join中的线程 → 抛InterruptedException,标记被清空为 false。 - 因此
catch (InterruptedException e)里若想继续退出,必须重新interrupt()恢复标记。
线程状态:Java 六态 vs 操作系统五态

Java 的 Thread.State 是六态,与 OS 五态(新建/就绪/运行/阻塞/终止)不是一回事:
| Java 状态 | 含义 | 进入方式 |
|---|---|---|
NEW |
创建但未 start() |
new Thread() |
RUNNABLE |
运行 + 就绪 + OS 层面的 IO 阻塞,JVM 统统算 RUNNABLE | start() / 被唤醒后抢到锁 |
BLOCKED |
等待 synchronized 的 Monitor 锁(EntryList) |
竞争 monitor 失败 |
WAITING |
无时限等待 | wait() / join() / LockSupport.park() |
TIMED_WAITING |
有时限等待 | sleep(n) / wait(n) / join(n) / parkNanos |
TERMINATED |
已结束 | run() 正常返回或异常终止 |
状态转换速记图:
start() 获取锁失败
NEW ──────────> RUNNABLE ─────────────────────> BLOCKED ──(抢到锁)──> RUNNABLE
│ │ ▲ ▲
wait()/join() │ │ sleep(n)/wait(n)/join(n) │ notify 后需重新抢锁 │
/park() │ │ /parkNanos └────────────────────┘
▼ ▼
WAITING TIMED_WAITING
│ │
notify/notifyAll/unpark/超时
└──┴────────> RUNNABLE ──(run 结束)──> TERMINATED
关键区分 :
BLOCKED与WAITING都不占用 CPU,但唤醒路径不同 ------BLOCKED 由"锁释放"唤醒,WAITING 由notify/unpark唤醒,且唤醒后不直接获得锁,要先进入 EntryList 重新竞争(即先变 BLOCKED 再变 RUNNABLE)。
三 JMM 与三大特性:可见性、原子性、有序性
三大特性与保障手段
| 特性 | 问题根源 | 保障手段 |
|---|---|---|
| 原子性 | 操作被线程调度打断(如 i++ 是读-改-写三步) |
synchronized、锁、CAS 原子类 |
| 可见性 | 线程把共享变量拷到工作内存,改了别的线程不一定立刻看到 | volatile、synchronized、锁、final |
| 有序性 | 编译器/CPU 指令重排 | volatile(内存屏障)、synchronized、happens-before |
volatile:两层语义 + 一个不能
- 可见性 :对 volatile 变量写指令后加写屏障 (
sfence,HotSpot 用lock前缀指令实现),强制刷主存;读指令前加读屏障,强制从主存加载。 - 有序性:写屏障禁止它之前的代码被重排到屏障后;读屏障禁止它之后的代码被重排到屏障前。
- 不保证原子性 :
volatile int i; i++依然是错的,因为无法阻止"指令交错"。
经典场景:DCL 单例为什么必须 volatile
java
// 字节码:17:new → 20:dup → 21:invokespecial <init> → 24:putstatic INSTANCE
// 21(初始化) 和 24(赋值引用) 可能被重排 → 24 先执行时,别的线程拿到"半初始化"对象
private static volatile Singleton INSTANCE; // JDK 5 起 volatile 语义增强后才真正有效
happens-before 规则
- 程序次序规则:一个线程内,书写在前的操作 happens-before 书写在后的操作
- volatile 规则:volatile 写 happens-before 后续对该变量的读
- 锁规则:解锁 happens-before 后续加锁
- start 规则 :
thread.start()happens-before 该线程的任何动作 - join 规则 :线程的所有动作 happens-before
join()返回 - 传递性:A→B、B→C ⇒ A→C
- 线程中断规则:
interrupt()happens-before 被中断线程检测到中断 - 对象终结规则:构造函数结束 happens-before
finalize()
final 的可见性
final 字段赋值后 JVM 会插入写屏障 ,保证其它线程不会读到 0 / null 的默认值------这是"不可变对象天然线程安全"的底层依据之一。
什么是线程安全?
当多个线程访问某个类时,不管运行时环境采用何种调度方式、这些线程如何交替执行 ,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,则称这个类是线程安全的。
判断一个变量是否线程安全,看三件事:是否被多线程共享 、是否有写操作 、是否做了同步。
四 管程与 synchronized
synchronized 的三种用法
| 用法 | 锁对象 |
|---|---|
synchronized 实例方法 |
this(当前实例) |
synchronized 静态方法 |
类对象 Xxx.class |
synchronized(obj) {} 代码块 |
指定的 obj |
铁律 :多个线程的锁必须是同一个对象才有互斥效果 ,且所有线程都要上锁。
字节码层面 :同步代码块 = monitorenter / monitorexit 指令对;同步方法靠方法表里的 ACC_SYNCHRONIZED 标志(指令里看不到)。
Monitor(管程)结构
每个 Java 对象都可以关联一个 Monitor:
┌───────────────────────── Monitor ─────────────────────────┐
│ │
Owner │ 持有锁的线程(同一时刻只能有一个) │
│ │
EntryList│ 竞争锁失败的线程 → BLOCKED(等 Owner 释放时被唤醒) │
│ │
WaitSet │ 调用 wait() 的线程 → WAITING(等 notify/notifyAll) │
└───────────────────────────────────────────────────────────┘
对象头 Mark Word 与锁状态(64 位 HotSpot)
| 状态 | Mark Word 布局 | 锁标志位 |
|---|---|---|
| Normal(无锁) | `hashcode:31 | age:4 |
| Biased(偏向锁) | `thread:54 | epoch:2 |
| Lightweight Locked | `ptr_to_lock_record:62 | 00` |
| Heavyweight Locked | `ptr_to_heavyweight_monitor:62 | 10` |
| Marked for GC | (空) | 11 |
可用 JOL(
org.openjdk.jol:jol-core)打印对象头验证。
锁升级完整链路(JDK 6~14 )
无锁 ──① 首次加锁(CAS 写线程ID)──> 偏向锁 ──② 出现第二个线程竞争──> 轻量级锁
│
③ 自旋失败 / 竞争激烈
▼
重量级锁(Monitor,OS mutex)
| 阶段 | 机制 | 关键点 |
|---|---|---|
| 偏向锁 | 首次用 CAS 把 Thread ID 写进 Mark Word,之后只需比对线程 ID,零 CAS | 偏向锁解锁后线程 ID 仍留在对象头 ;Mark Word 没有存 hashCode 的位置,所以调用 hashCode() 会强制撤销偏向锁 |
| 轻量级锁 | 线程栈帧建 Lock Record,CAS 把 Mark Word 换成指向 Lock Record 的指针 | 重入时再压一条 Lock Record(值为 null)作计数;CAS 失败 ⇒ 锁膨胀 |
| 锁膨胀 | 申请 Monitor,Mark Word 指向 Monitor 指针,竞争线程进 EntryList 阻塞 | 解锁时 CAS 恢复失败 ⇒ 走重量级解锁流程 |
| 自旋优化 | 重量级锁竞争时先自旋,避免立即阻塞 | Java 6 起自适应自旋 (按上次成功情况动态调整);Java 7 起无法关闭自旋;单核自旋纯属浪费 |
偏向锁的四种撤销场景:
- 调用对象的
hashCode()→ 撤销偏向(轻量锁记在 Lock Record、重量锁记在 Monitor) - 其它线程使用该对象 → 升级为轻量级锁
- 调用
wait()/notify()→ 直接膨胀为重量级锁(wait/notify 必须依托 Monitor) - 批量重偏向 / 批量撤销:同一类对象撤销偏向 超过 20 次 → JVM 对该类"批量重偏向";超过 40 次 → 该类所有对象(含新建)不可偏向
相关 JVM 参数 :-XX:+UseBiasedLocking、-XX:BiasedLockingStartupDelay=0(取消默认 4 秒延迟)、-XX:+EliminateLocks(锁消除,默认开启)、-XX:+UseSpinning。
【版本边界 · 必考】 :JDK 15(JEP 374)已默认禁用并废弃偏向锁,JDK 18 起相关参数被标记为 obsolete。
- JDK 6~14:无锁 → 偏向 → 轻量 → 重量(四态)
- JDK 15+(含 17/21/25):无锁 → 轻量级锁 → 重量级锁(三态)
- 原因:现代应用多为高并发,偏向锁收益下降、撤销需 STW、维护成本高。如果你在 2026 年面试还把"偏向锁"当默认路径讲,会直接暴露答案是 2019 年的。
锁消除与锁粗化(JIT 优化)
- 锁消除 :JIT 通过逃逸分析 发现锁对象不会逃出当前线程(如方法内局部
new Object()),直接去掉锁。实测:加锁与不加锁均约 1.5 ns/op ;用-XX:-EliminateLocks关闭后为 16.9 ns/op。 - 锁粗化 :对同一对象连续多次加锁/解锁(如循环里
synchronized)合并成一次,减少开销。
线程八锁(经典例题,本质是"锁的到底是谁")
答题模板:
- 先看锁对象:
this还是Xxx.class------两者互不影响 synchronized方法与无锁方法不互斥- 同一对象的多个
synchronized实例方法互斥 - 静态方法上的
synchronized锁的是类对象,与实例锁不是一把锁


情况3:3 1s 12 或 23 1s 1 或 32 1s 1
java
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}

情况5:2 1s 后 1
java
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}


情况8:1s 后12, 或 2 1s后 1
java
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
wait / notify / notifyAll
| 要点 | 说明 |
|---|---|
| 调用前提 | 必须在 synchronized 内 ,否则抛 IllegalMonitorStateException |
wait() |
释放锁 并进入 WaitSet(WAITING);被唤醒后仍需重新抢锁 |
wait(n) |
带超时版本 |
notify() |
随机唤醒 WaitSet 中一个 |
notifyAll() |
唤醒 WaitSet 中全部 (生产推荐) |
正确姿势(防虚假唤醒):
java
synchronized (lock) {
while (条件不满足) { // ❗必须用 while,不能用 if
lock.wait();
}
// 干活
lock.notifyAll();
}
sleep vs wait(送分题):
| 维度 | sleep |
wait |
|---|---|---|
| 所属 | Thread 静态 |
Object 实例 |
| 释放锁 | 否 | 是 |
| 使用前提 | 任意位置 | 必须在同步块内 |
| 唤醒 | 时间到 | notify/notifyAll |
| 状态 | TIMED_WAITING | WAITING / TIMED_WAITING |
park / unpark(LockSupport)
每个线程持有一个 Parker 对象,内部有许可计数器 _counter(0 表示无许可):
park() : _counter == 0 → 阻塞;_counter == 1 → 消费掉,直接返回
unpark(t): _counter 置 1,若线程正阻塞则唤醒它
相比 wait/notify 的三大优势:
- 以线程为单位,不需要锁对象、不需要同步块
- 可以先 unpark 再 park(许可不会丢),wait/notify 顺序反了会永久等待
- 更底层,AQS 全部基于它
注意 :连续多次 unpark 只保留一份许可,不会累加。
join 的原理(保护性暂停)
java
// t1.join() 语义等价于:
synchronized (t1) {
while (t1.isAlive()) { t1.wait(0); }
}
活跃性:死锁、活锁、饥饿
| 问题 | 定义 | 解法 |
|---|---|---|
| 死锁 | 多线程互相持有对方需要的锁,永久阻塞 | 按固定全局顺序加锁 (如按账户 id 排序);用 tryLock(timeout) |
| 活锁 | 线程互相"谦让"不断重试,看似在跑实则无进展 | 引入随机退避时间 |
| 饥饿 | 某线程长期抢不到 CPU/锁 | 公平锁 / 合理线程数 |
死锁四必要条件 (缺一不可):互斥、占有且等待、不可抢占 、循环等待。破坏任一即可预防。
排查 :jstack <pid>,JVM 会直接打印 Found one Java-level deadlock 及互相等待的锁与栈。
哲学家就餐(经典场景题):5 人 5 筷,全拿左筷 → 死锁。两种解法:
- 信号量限流 :
new Semaphore(4),最多 4 人同时就餐 → 破坏循环等待 - 奇偶异序:奇数号先拿左、偶数号先拿右 → 破坏循环等待
参考资料