目录
[1. 偏向锁(Biased Locking)](#1. 偏向锁(Biased Locking))
[2. 轻量级锁(Lightweight Locking)](#2. 轻量级锁(Lightweight Locking))
[3. 重量级锁(Heavyweight Locking)](#3. 重量级锁(Heavyweight Locking))
[4. 自旋锁(Spin Lock)](#4. 自旋锁(Spin Lock))
[5. 锁消除(Lock Elimination)](#5. 锁消除(Lock Elimination))
[6. 锁粗化(Lock Coarsening)](#6. 锁粗化(Lock Coarsening))
[三、JMM 内存模型](#三、JMM 内存模型)
[为什么要 JMM](#为什么要 JMM)
[happens-before 原则](#happens-before 原则)
一、并发和并行
三种执行方式
1. 顺序执行(串行)
程序一条指令接一条指令地执行,前一个任务没做完,后一个就不能开始。
// 顺序执行:先做完 taskA,再做 taskB
public static void main(String[] args) {
taskA(); // 耗时 3s
taskB(); // 耗时 3s
// 总耗时:6s
}
2. 并发执行(Concurrent)
同一时间段内,多个任务交替执行。宏观上"同时进行",微观上是 CPU 在多个任务之间快速切换(时间片轮转)。单核 CPU 上的"多任务"本质都是并发。
// 并发执行:两个任务交替使用 CPU
Thread t1 = new Thread(() -> taskA());
Thread t2 = new Thread(() -> taskB());
t1.start();
t2.start();
// 单核 CPU 上:总耗时接近 6s,但期间可以响应用户输入(不被 taskA 阻塞)
3. 并行执行(Parallel)
同一时刻,多个任务真正地同时执行。并行依赖多核 CPU------每个核心跑一个任务,互不干扰。
单核 CPU(并发): 四核 CPU(并行):
CPU: A A B B A A B B Core1: A A A A A A
↑ 时间片轮转切换 Core2: B B B B B B
(A、B 真正同时运行)
对比总结
| 维度 | 顺序执行 | 并发 | 并行 |
|---|---|---|---|
| 任务执行方式 | 一个做完再做下一个 | 交替执行(切换) | 真正同时执行 |
| 依赖硬件 | 单核即可 | 单核即可 | 必须多核 |
| 关注点 | 简单、可预测 | 任务调度、吞吐量 | 计算能力、速度 |
| 典型场景 | 脚本、批处理 | Web 服务器响应请求 | 大数据计算、并行流 |
一句话区分:
并发 是"轮流处理多件事"的能力(结构设计),并行是"同时处理多件事"的能力(硬件能力)。并发是逻辑层的概念,并行是物理层的概念。
面试常问
- Q:单核 CPU 能实现并行吗?
A:不能。单核任一时刻只能执行一条指令,只能通过时间片切换实现并发。 - Q:多线程一定是并行的吗?
A:不一定。线程数多于核心数时,依然是并发切换为主;只有在多核上多个线程被同时调度到不同核心时,才是并行。
二、锁机制
Java 中的 synchronized 并不是一开始就使用重量级互斥锁,而是有一套锁升级路径:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(锁只能升级,不能降级,JDK 15 之前如此)
1. 偏向锁(Biased Locking)
适用场景:始终只有一个线程访问同步块。很多场景下锁根本不存在竞争(比如一个 Vector 只被一个线程用),加锁解锁的开销纯属浪费。
工作方式:
- 第一次线程访问同步块时,在对象头的 Mark Word 中记录当前线程 ID;
- 之后该线程再进入,只需比对 Mark Word 中的线程 ID 是否是自己,无需任何 CAS 或互斥操作;
- 一旦有第二个线程来竞争,偏向锁升级为轻量级锁。
本质:用"记住是谁"代替"反复确认"。 把同步开销降到接近零。
2. 轻量级锁(Lightweight Locking)
适用场景:多个线程交替执行同步块,但同一时刻没有实际竞争(竞争不激烈、持有时间短)。
工作方式:
- 线程进入同步块前,在当前线程栈帧中开辟一块 Lock Record 空间;
- 将对象 Mark Word 拷贝到 Lock Record 中;
- 通过 CAS 尝试把对象 Mark Word 替换为指向 Lock Record 的指针,成功则获得锁;
- 失败说明有竞争,线程先自旋等待;
- 自旋一定次数(自适应自旋)后仍失败,膨胀为重量级锁。
3. 重量级锁(Heavyweight Locking)
适用场景:实际存在多个线程同时竞争。
- 依赖操作系统的 Mutex(互斥量) 实现;
- 涉及用户态 ↔ 内核态切换,开销大(线程的挂起与唤醒都要操作系统介入);
- 优点是不自旋消耗 CPU,竞争激烈时更稳定。
4. 自旋锁(Spin Lock)
轻量级锁膨胀前的过渡手段:拿不到锁时,线程不挂起,而是空转几个循环,赌"锁马上就释放了"。
- 优点:避免了内核态切换的开销,如果锁很快释放,整体性能更高;
- 缺点:自旋期间占用 CPU;如果锁被长时间持有,纯浪费 CPU;
- JDK 6 引入自适应自旋:根据上一次在同一个锁上的自旋时间和持有者状态,动态调整自旋次数。
锁机制对比
| 维度 | 偏向锁 | 轻量级锁 | 重量级锁 |
|---|---|---|---|
| 适用场景 | 只有一个线程访问 | 多线程交替、无实际竞争 | 多线程实际竞争 |
| 实现方式 | Mark Word 记录线程 ID | CAS + 栈帧 Lock Record | OS Mutex |
| 加锁开销 | 几乎为零 | CAS 操作(用户态) | 内核态切换 |
| 阻塞挂起 | 否 | 否(自旋) | 是 |
| 状态标识 | Mark Word 偏向位=1 | Mark Word 指向栈帧 | Mark Word 指向 monitor |
5. 锁消除(Lock Elimination)
JIT 编译器在运行时,通过逃逸分析 发现某个锁对象根本不可能被其他线程访问,于是直接把锁删掉。
public String concat(String s1, String s2) {
// StringBuffer 的方法都是 synchronized 的
// 但 sb 是局部变量,不会逃逸出这个方法
// JIT 会消除掉所有内部的加锁操作,性能等同 StringBuilder
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
6. 锁粗化(Lock Coarsening)
通常我们建议缩小同步块,但反过来:如果一系列连续操作都对同一个对象反复加锁解锁 (比如循环里调 StringBuffer.append),JIT 会把锁的范围扩大(粗化)到整个操作序列之外,只加一次锁。
for (int i = 0; i < 100; i++) {
synchronized (lock) { // 优化前:加锁 100 次
doSomething(i);
}
}
// JIT 粗化后等价于:
synchronized (lock) { // 只加锁 1 次
for (int i = 0; i < 100; i++) {
doSomething(i);
}
}
三、JMM 内存模型
为什么要 JMM
硬件层面,CPU 有多级缓存(L1/L2/L3),每个核心有自己的缓存副本。多线程下会出现两个致命问题:
- 可见性:线程 A 修改了变量,线程 B 可能看到的还是旧值(写在 A 的工作内存里,没刷到主存);
- 有序性 :编译器和 CPU 为了优化会进行指令重排序,多线程下执行顺序可能与代码顺序不一致。
JMM(Java Memory Model,Java 内存模型) 是 JVM 定义的一套抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让 Java 程序在所有平台上都能获得一致的内存可见性保证。
抽象结构:主内存与工作内存
Java 内存模型(JMM)
┌─────────────────────────────────────┐
│ 主内存(Main Memory) │
│ 所有共享变量存放在这里 │
└─────────────────────────────────────┘
↑ load / save ↑ load / save
┌───────────────┐ ┌───────────────┐
│ 线程 A 工作内存 │ │ 线程 B 工作内存 │
│ 变量的私有副本 │ │ 变量的私有副本 │
└───────────────┘ └───────────────┘
- 主内存:所有线程共享,存放共享变量(实例字段、静态字段);
- 工作内存:每个线程私有,保存了该线程使用到的变量的主内存副本;
- 线程对变量的所有操作(读、写)都必须在工作内存 中进行,不能直接读写主内存;
- 线程之间无法直接访问对方的工作内存,传递必须经过主内存。
注意:JMM 的主内存/工作内存与 JVM 的堆/栈是不同层次的划分,大致上主内存对应堆,工作内存对应栈+寄存器/缓存,但不要画等号。
三大特性
| 特性 | 含义 | 解决手段 |
|---|---|---|
| 原子性 | 操作不可分割,要么全部执行,要么不执行 | synchronized、Lock、原子类(CAS) |
| 可见性 | 一个线程的修改,其他线程立即可见 | volatile、synchronized、final |
| 有序性 | 禁止指令重排序带来的乱序 | volatile、synchronized(锁保证串行语义) |
happens-before 原则
JMM 不可能禁止所有重排序(否则性能崩溃),于是定义了一套天然有序的规则------只要两个操作之间存在 happens-before 关系,前一个操作的结果就对后一个操作可见,且不会重排序:
- 程序顺序规则:单线程内,前面的操作 happens-before 后面的操作;
- 锁规则:解锁 happens-before 后续对同一把锁的加锁;
- volatile 规则:对 volatile 变量的写 happens-before 后续的读;
- 线程启动规则 :
Thread.start()happens-before 该线程内的所有操作; - 线程终止规则 :线程内的所有操作 happens-before 其他线程检测到它终止(
join()返回); - 传递性:A hb B,B hb C ⇒ A hb C。
核心思想:为程序员提供强易用性(看起来顺序执行),为编译器/处理器提供最大优化自由(在规则内随便重排)。
一个经典的可见性问题
public class VisibilityDemo {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// 可能永远停不下来:
// 读的是本线程工作内存中的旧值
}
System.out.println("线程结束");
}).start();
Thread.sleep(1000);
flag = false; // 主线程修改,子线程可能看不到
}
}
加上 volatile 即可修复:
private static volatile boolean flag = true;
// volatile 保证:写立即刷回主内存 + 使其他线程的缓存副本失效
四、总结
| 知识点 | 一句话记忆 |
|---|---|
| 并发 vs 并行 | 并发是"轮流"(逻辑),并行是"同时"(硬件) |
| 锁升级 | 无锁 → 偏向锁 → 轻量级锁 → 重量级锁,逐级膨胀 |
| 偏向锁 | 只有一个线程访问,Mark Word 记线程 ID,加锁近零开销 |
| 轻量级锁 | CAS + 自旋,避免内核态切换 |
| 重量级锁 | OS Mutex,内核态切换,竞争激烈时使用 |
| 锁消除/粗化 | JIT 基于逃逸分析删掉无用锁 / 合并连续锁 |
| JMM | 主内存 + 工作内存的抽象模型,解决可见性与有序性 |
| happens-before | 判断多线程操作是否可见、是否有序的唯一依据 |