Java 多线程三大特性、volatile、synchronized、ReentrantLock 深度剖析
并发编程是后端面试的重中之重。很多同学在写业务代码时很少手写多线程,对并发底层理解浮于表面。 并发的根基就是可见性、原子性、有序性三大特性,volatile、synchronized、ReentrantLock 都是用来解决三大特性问题的工具。
提问:volatile 能不能保证原子性?synchronized 锁优化(偏向锁、轻量级锁、重量级锁)?synchronized 和 ReentrantLock 区别?可重入、公平锁、自旋锁原理。
本文从底层原理、JMM 内存模型、三大特性讲起,逐个拆解 volatile、synchronized、ReentrantLock,附带代码示例、踩坑案例、对比总结,一次性吃透这块核心知识。
前言
单核 CPU 时代,程序串行执行,不存在线程安全问题。 现代 CPU 都是多核,操作系统通过时间片轮转实现线程切换。同时 CPU 有高速缓存、指令重排优化,这就导致多线程共享变量会出现各种诡异问题:修改不可见、执行结果错乱、代码执行顺序和写的不一样。 Java 内存模型(JMM,Java Memory Model),是一套抽象规范,屏蔽不同操作系统、CPU 硬件差异,定义线程、主内存、工作内存之间的读写规则。
JMM 抽象模型:所有共享变量存储在主内存;每个线程有自己的工作内存(CPU 缓存寄存器抽象)。线程读写共享变量,不是直接操作主内存,而是先拷贝副本到工作内存,操作完成后再刷回主内存。 线程安全问题根源:多线程同时读写共享可变变量。
一、并发三大核心特性(基础中的基础)
1. 可见性
定义:一个线程修改共享变量的值,其他线程能够立刻感知到这个修改。 问题:CPU 缓存机制。线程 A 修改变量,只更新到自己 CPU 缓存,没有立刻刷入主内存;线程 B 读取变量,读到的还是旧副本。
典型场景:普通变量不具备可见性。
// 可见性问题示例
public class VisibilityDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
// 死循环
}
System.out.println("线程感知到flag变化,退出循环");
}).start();
Thread.sleep(1000);
flag = true;
System.out.println("主线程修改flag=true");
// 很多时候,子线程永远不会退出!
}
}
原因:JIT 优化,子线程把 flag 缓存到寄存器,不再从主内存读取。
解决方案:volatile 修饰变量,保证可见性。
2. 原子性
定义:一组操作,要么全部执行成功,要么全部不执行,执行过程不可被线程中断。
经典例子:
i++,看上去一行代码,实际拆成三步:
- 读 1. 读取 i 的值;
- i+1;
- 写回 i。 这三步不是原子操作,多线程并发执行会丢失更新。
public class AtomicDemo {
private static int count = 0;
public static void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {for(int i=0;i<10000;i++) add();});
Thread t2 = new Thread(() -> {for(int i=0;i<10000;i++) add();});
t1.start();t2.start();
t1.join();t2.join();
System.out.println(count); // 结果大概率小于20000
}
}
普通变量、volatile无法保证原子性。原子性需要锁 / CAS / 原子类(AtomicInteger)保障。
3. 有序性
定义:代码按照程序员编写的顺序执行。 CPU 和 JVM 为了提升执行效率,会做指令重排。只要不改变单线程内程序结果,可以调整指令执行顺序。
指令重排在单线程无害;多线程共享变量场景会出现诡异 bug。 经典案例:双重检查锁 DCL 单例不加 volatile,指令重排导致半初始化对象逸出。
三大特性小结:
| 特性 | 含义 | 产生原因 | 解决方案 |
|---|---|---|---|
| 可见性 | 一个线程修改,其他线程立刻看见 | CPU 高速缓存 | volatile / synchronized / ReentrantLock |
| 原子性 | 操作不可拆分,不能中途被打断 | 线程切换 | synchronized / ReentrantLock / CAS 原子类 |
| 有序性 | 禁止指令乱序执行 | CPU、JVM 指令重排 | volatile / synchronized / ReentrantLock |
二、volatile 关键字
作用
- 保证可见性:写操作后,强制刷新到主内存;读操作直接从主内存读取,清空本地缓存副本。
- 禁止指令重排:volatile 读写内存屏障,阻止编译器、CPU 对 volatile 变量前后指令重排序。
- 不保证原子性!这是最高频考点。
volatile i++依旧线程不安全。
内存屏障(简要理解)
volatile 底层依靠内存屏障实现:
- 写 volatile:在写指令前后插入写屏障,前面的指令不会重排到 volatile 写之后;写完成强制刷入主内存。
- 读 volatile:在读指令前后插入读屏障,后面指令不会重排到 volatile 读之前,强制从主内存加载。
volatile 适用场景
场景 1:状态标记变量(可见性),例如上面 flag 停止标记。 场景 2:双重检查锁 DCL 单例模式,防止指令重排。
public class Singleton {
// 必须加volatile
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
new Singleton()分为三步:
- 分配对象内存;
- 初始化对象;
- 将 instance 引用指向内存地址。 不加 volatile:指令重排可能变成 1→3→2。线程 A 执行 1、3,还没初始化;线程 B 判断 instance 不为 null,拿到半初始化对象,引发严重 bug。volatile 禁止这三步重排。
volatile 不适用场景
- 计数场景:
count++,复合操作,无法保证原子性。 - 多线程同时写同一个共享变量。
三、synchronized 内置锁(重量级锁,JDK1.6 做了锁升级优化)
synchronized 是 Java 内置关键字,可重入互斥锁,可以修饰方法或者代码块。
互斥:同一时间,只允许一个线程获取锁,执行同步代码。保证原子性、可见性、有序性。
使用方式
1.修饰实例方法:锁对象是当前 this 对象
public synchronized void test(){}
2.修饰静态方法:锁对象是类的 Class 对象
public static synchronized void test(){}
3.修饰代码块:手动指定锁对象(推荐,锁粒度更小)
synchronized (obj) {
// 同步代码
}
JDK1.6 锁升级(核心)
synchronized 不再是单纯重量级锁,会自动升级,不可逆:偏向锁 → 轻量级锁 → 重量级锁。
- 偏向锁 大多数场景:锁长时间只被同一个线程持有。偏向锁:消除无竞争场景下 CAS 操作。 线程获取锁,在对象头 Mark Word 记录线程 ID。后续该线程再次拿锁,无需 CAS,直接拿到。 当出现其他线程竞争,偏向锁撤销,升级轻量级锁。
- 轻量级锁 竞争不激烈:线程自旋(循环尝试获取锁),不阻塞。依靠 CAS 自旋抢锁。 优点:线程不阻塞,不需要内核态切换; 缺点:自旋消耗 CPU。自旋多次拿不到锁,升级重量级锁。
- 重量级锁 底层依赖操作系统互斥锁(Mutex)。抢锁失败的线程进入阻塞队列,操作系统挂起线程。 线程挂起 / 唤醒需要用户态↔内核态切换,开销大。
锁升级单向:只能升级,不能降级。
synchronized 特性
- 可重入:同一个线程,已经持有锁,可以再次获取同一把锁(避免死锁)
- 自动释放锁:代码正常退出 / 抛出异常,JVM 自动释放锁,不会死锁
- 非公平锁:抢锁不按先来后到,后到线程可能抢先拿到锁
- 同时保证三大特性:原子性、可见性、有序性
四、ReentrantLock 显式锁(java.util.concurrent.locks)
ReentrantLock 是API 层面的锁,不是 JVM 内置。实现 Lock 接口,底层基于 AQS(AbstractQueuedSynchronizer)。
AQS:底层维护一个 state 状态变量 + 等待队列,state=0 无锁,state>0 代表锁被持有。
基础使用模板
Lock lock = new ReentrantLock();
lock.lock();
try{
// 同步业务代码
}finally {
// 必须finally手动释放锁!!
lock.unlock();
}
重点:必须手动unlock()释放锁,如果代码异常忘记释放锁,会永久死锁!
ReentrantLock 核心特性
- 可重入:和 synchronized 一样,同一个线程可多次获取锁,state 累加。
- 支持公平锁 / 非公平锁(synchronized 只能非公平)
// true:公平锁,线程按等待顺序获取
ReentrantLock fairLock = new ReentrantLock(true);
// false:非公平锁(默认),性能更高
ReentrantLock unfairLock = new ReentrantLock(false);
公平锁:线程排队,严格 FIFO;缺点:大量线程切换,性能低。 非公平锁:上来先 CAS 抢锁,不排队,吞吐量更高,默认推荐。
- 可中断:
lockInterruptibly(),等待锁的时候可以响应中断。synchronized 等待锁时无法被中断。 - 尝试获取锁,支持超时:
tryLock()、tryLock(time,unit),拿不到锁立刻返回 false,不会死等。synchronized 拿不到锁会一直阻塞。 - 条件变量 Condition:一把锁可以绑定多个 Condition,精准唤醒线程,替代 wait/notify。synchronized 只有一个等待队列,notify 随机唤醒。
五、synchronized VS ReentrantLock 对比
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置关键字,底层 C++ 实现 | API 层面锁,基于 AQS,Java 代码实现 |
| 锁释放 | 自动释放,异常自动解锁 | 手动 unlock (),忘记释放会死锁 |
| 可重入 | 支持 | 支持 |
| 锁类型 | 只能非公平锁 | 支持公平 / 非公平锁可选 |
| 中断等待 | 等待锁时不可中断 | lockInterruptibly () 可中断 |
| 超时抢锁 | 不支持 | tryLock (time) 支持超时尝试获取 |
| 条件变量 | wait/notify,单个等待队列 | 多个 Condition,精准唤醒 |
| 锁升级 | 偏向→轻量级→重量级,自动优化 | 无锁升级机制,CAS + 队列 |
| 性能 | JDK1.6 优化后,轻量场景性能接近 | 高并发竞争场景,功能更灵活 |
选型建议:
- 简单同步场景,优先 synchronized,代码简洁,自动释放锁,不易出错;
- 需要公平锁、超时锁、可中断、多条件唤醒 → 选择 ReentrantLock。
六、wait () /notify () 和 Condition 补充
wait、notify/notifyAll 必须在 synchronized 同步代码块内调用,属于 Object 方法。
- wait:释放锁,线程进入等待队列;
- notify:随机唤醒一个等待线程;notifyAll 唤醒全部。 缺点:无法区分等待条件,唤醒是随机的。 Condition 是 ReentrantLock 配套:
await()、signal()、signalAll()。 一把锁可以创建多个 Condition,不同条件的线程放在不同等待队列,可以精准唤醒,更加灵活。
七、开发实战踩坑清单
- 误以为 volatile 保证原子性,拿 volatile 做计数。
count++依旧并发不安全。 - DCL 单例忘记加 volatile,指令重排导致半初始化对象泄露。
- ReentrantLock unlock 写在 try 代码块,异常情况下无法释放锁,死锁。 规范:lock 写在 try 外面,unlock 放在 finally。
- synchronized 锁字符串常量、包装类。常量池复用对象,不同地方共用同一把锁,意外锁竞争。推荐 new Object () 作为独立锁对象。
- 锁对象发生引用替换。synchronized 锁对象变量指向新对象,锁失效。
Object lock = new Object();
synchronized (lock){
lock = new Object(); // 锁对象被替换!
}
6.wait () 不在同步代码块调用,直接抛出 IllegalMonitorStateException。
7.原子类 AtomicInteger 底层 CAS,只能保证单个变量原子性,复合操作依然不安全。
八、三大锁的能力汇总表
| 原子性 | 可见性 | 有序性 | 可重入 | |
|---|---|---|---|---|
| volatile | ❌ | ✅ | ✅ | 无(不是锁) |
| synchronized | ✅ | ✅ | ✅ | ✅ |
| ReentrantLock | ✅ | ✅ | ✅ | ✅ |
九、问答
Q1:JMM 是什么?三大特性是什么?
A:JMM 是 Java 内存模型抽象规范,定义多线程共享变量读写规则。三大特性:可见性、原子性、有序性。
Q2:volatile 原理,能保证原子性吗?使用场景?
A:volatile 依靠内存屏障保证可见性、禁止指令重排;不能保证原子性。适合状态标记、DCL 单例。
Q3:synchronized 锁升级过程?
A:偏向锁 → 轻量级锁 → 重量级锁,升级不可逆。偏向锁适合单线程无竞争;轻量级锁 CAS 自旋;重量级锁操作系统互斥锁,线程阻塞。
Q4:synchronized 和 ReentrantLock 区别?
A:synchronized 是 JVM 内置锁,自动释放锁,只能非公平;ReentrantLock 是 AQS 实现显式锁,手动 unlock,支持公平锁、超时锁、可中断、多 Condition。简单场景优先 synchronized。
Q5:为什么 ReentrantLock 一定要在 finally 释放锁?
A:如果业务代码抛出异常,不执行 unlock,锁永远不会释放,其他线程永久阻塞,死锁。
Q6:什么是可重入锁?作用?
A:同一个线程拿到锁之后,可以再次获取同一把锁。防止递归调用同步代码时,自己把自己死锁。synchronized、ReentrantLock 都是可重入锁。
Q7:轻量级锁自旋的缺点?
A:自旋会持续占用 CPU。大量线程长时间自旋,CPU 飙升。竞争激烈升级重量级锁。
结语
并发三大特性是整个 Java 并发体系的地基。volatile 只解决可见性、有序性,无法原子性;synchronized 和 ReentrantLock 可以同时保障三大特性,但各有取舍。 并发编程最大难点不是 API 调用,而是识别共享可变变量、判断竞态条件。
写并发代码优先考虑:尽量避免共享可变状态,无锁方案优先;必须共享时,选合适锁机制,规避复合操作、锁对象变更、忘记释放锁等坑。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)