synchronized与volatile
本篇定位
并发安全的两大关键字:synchronized 解决"互斥"(同一时刻只有一个线程进临界区),volatile 解决"可见性与有序性"(但不保证原子性)。本篇的地基是 JMM 与内存可见性——先理解"工作内存/主内存"模型再回来,所有现象都解释得通。
先看问题:不加锁会怎样
public class UnsafeCounter {
static int count = 0;
public static void main(String[] args) throws Exception {
Runnable r = () -> {
for (int i = 0; i < 10000; i++) count++; // 非原子:读→加→写
};
Thread t1 = new Thread(r), t2 = new Thread(r);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(count); // 常见输出 12000~18000,几乎不会是 20000
}
}
count++ 是三条字节码指令(读字段、加 1、写字段),两个线程交错执行就会互相覆盖——这就是竞态条件(Race Condition)。
synchronized:三种用法,锁的是同一个东西吗
public class SyncDemo {
// 1. 修饰实例方法:锁的是当前实例 this
public synchronized void instanceMethod() { }
// 2. 修饰静态方法:锁的是 Class 对象(全局唯一)
public static synchronized void staticMethod() { }
// 3. 修饰代码块:锁的是括号里的对象
private final Object lock = new Object();
public void blockMethod() {
synchronized (lock) {
// 临界区
}
}
}
核心认知:synchronized 是可重入的互斥锁,任何对象都可以当锁(对象头 Mark Word 记录锁状态)。注意——两个线程用不同的锁对象就互不排斥,这既是灵活性也是 bug 来源。
字节码层:同步代码块靠 monitorenter/monitorexit 指令,同步方法靠 ACC_SYNCHRONIZED 标志位;每个对象关联一个 monitor(管程),线程抢的其实就是 monitor 的持有权。
锁升级(JDK 6 优化,面试必考)
对象锁状态随竞争激烈程度单向升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
| 级别 | 适用场景 | 做法 |
|---|---|---|
| 偏向锁 | 始终只有一个线程访问 | 在对象头记下线程 ID,之后该线程进入无需任何同步操作 |
| 轻量级锁 | 多线程交替执行、无实质竞争 | 栈上锁记录 + CAS 把对象头指向栈帧;失败则自旋几次 |
| 重量级锁 | 竞争激烈 | 依赖操作系统 mutex,涉及用户态/内核态切换,线程阻塞挂起 |
注意:JDK 15 起偏向锁默认禁用并逐步移除(维护成本高于收益),面试提一句"新版本已弃用偏向锁"是加分项。
死锁:锁的极端后果(面试必考)
本节定位
讲完"锁怎么用",必须讲"锁用错了会怎样"。死锁是 synchronized 互斥性的直接代价,四条件+避免手段+现场排查是面试三板斧。
一、两分钟复现一个死锁
public class DeadLockDemo {
static final Object A = new Object();
static final Object B = new Object();
public static void main(String[] args) throws Exception {
new Thread(() -> {
synchronized (A) {
Thread.sleep(100); // 给对方抢到 B 的时间窗
synchronized (B) { System.out.println("t1 拿到 A+B"); }
}
}, "t1").start();
new Thread(() -> {
synchronized (B) {
Thread.sleep(100);
synchronized (A) { System.out.println("t2 拿到 B+A"); }
}
}, "t2").start();
}
}
t1 持有 A 等 B,t2 持有 B 等 A——两边永远等下去。加锁顺序不一致就是最典型的死锁成因。
二、产生的四个必要条件(Coffman 条件,缺一不可)
| 条件 | 含义 | 对应的破坏手段 |
|---|---|---|
| 互斥 | 资源同一时刻只能被一个线程持有 | 不可破坏(锁的本义) |
| 持有并等待 | 拿着一个锁的同时去等另一个 | 一次性申请所有资源(把多把锁的获取包进一个大 synchronized 或统一资源管理器) |
| 不可剥夺 | 锁只能由持有者主动释放 | synchronized 做不到;ReentrantLock.tryLock(timeout) 拿不到就放弃已持有的锁(见 04-Lock与AQS) |
| 循环等待 | t1 等 t2 的、t2 等 t1 的形成环 | 全局统一加锁顺序(所有线程都先拿 A 再拿 B,环就成不了) |
四个条件同时成立才死锁——所以工程上只需破坏任意一条。"固定加锁顺序"是最常用、成本最低的一条。
三、定位死锁:现场排查三板斧
- jstack(最常用):
jstack <pid>输出末尾直接给出结论Found one Java-level deadlock,并打印"Found 1 deadlock"的线程互相等待链; - jconsole / jvisualvm:Threads 页签点"检测死锁"按钮,图形化看到两个线程的 Monitor 状态;
- 代码级预警:ReentrantLock 的
tryLock+ 超时重试,或Lock.newCondition()带超时的 await——把"永久等待"变成"限时等待"。
顺带认识两个亲戚:活锁(线程没阻塞,但互相谦让导致谁也走不下去,如重试时都用相同退避节奏)和饥饿(低优先级线程永远抢不到锁,非公平锁的副作用)——三者统称"活跃性问题"。
四、勾连
- JUC 工具类如何从设计上绕开死锁:BlockingQueue 把"锁+等待"封装进队列,生产者消费者两边永远只碰队列不互相持锁,见 05-并发容器与工具类;
- 排查线上问题(含死锁)的系统动作见 02-内存溢出 的线上排查清单。
volatile:可见性 + 有序性,不保证原子性
// volatile 的典型正确用法:一写多读的状态标志
public class TaskRunner {
private static volatile boolean running = true;
public static void main(String[] args) throws Exception {
new Thread(() -> {
while (running) { /* 干活 */ }
System.out.println("看到 running=false,退出");
}).start();
Thread.sleep(100);
running = false; // 没有 volatile,子线程可能永远看不到这个修改
}
}
volatile 三件事:
- 可见性:写 volatile 变量立即刷回主内存,读时强制从主内存加载(MESI 缓存一致性协议 + 内存屏障);
- 禁止指令重排序:编译器和 CPU 不能把 volatile 读写与前后操作随意换序;
- 不保证原子性:
volatile int i; i++依然会丢更新——它不是锁。
happens-before 一句话:volatile 写 happens-before 后续的 volatile 读——这就是"写线程的修改对读线程可见"的规范表述(JMM 细节见 JMM 篇)。
双重检查锁单例(DCL)——volatile 最经典的面试题:
public class Singleton {
private static volatile Singleton instance; // volatile 必须加
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免每次加锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:防止重复创建
instance = new Singleton(); // 三步:分配内存→初始化→赋值引用
}
}
}
return instance;
}
}
不加 volatile 的 bug:instance = new Singleton() 可能被重排为"分配→赋值引用→初始化",另一线程在第一次检查时拿到未初始化完成的实例。
synchronized vs volatile 一句话总结
| 维度 | synchronized | volatile |
|---|---|---|
| 原子性 | ✅ 保证 | ❌ 不保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证 |
| 阻塞 | 可能阻塞 | 不阻塞 |
| 适用 | 写多读少的临界区 | 状态标志、一写多读 |
⬅️ 01-线程基础与生命周期 🏠 00-Java ➡️ 03-线程池
💬 评论