synchronized / volatile
什么是线程安全?
多个线程同时访问同一个对象,不管 JVM 如何调度,程序都能输出正确结果,就叫线程安全。
线程安全的本质是要保证三个核心特性:原子性、可见性、有序性。
不安全示例:多线程同时执行 count++,结果小于预期,因为操作不原子。
安全示例:AtomicInteger、ConcurrentHashMap、加锁后的方法。
synchronized 的作用是什么?
synchronized 是 Java 内置互斥锁,同一时刻只有一个线程能执行被保护的代码,保证:
- 原子性:代码块整体执行,不被打断
- 可见性:退出时修改立即刷回主内存
- 有序性:防止块内外指令重排序
java
public synchronized void add() {
count++;
}底层:每个对象有一个 Monitor,通过字节码
monitorenter/monitorexit获取和释放。
synchronized 修饰普通方法、静态方法、代码块有什么区别?
三种写法锁的对象不同:
| 写法 | 锁对象 | 范围 |
|---|---|---|
| 普通方法 | this | 同一实例的线程互斥 |
| 静态方法 | Foo.class | 全局所有实例互斥 |
| 代码块 | 括号里的对象 | 粒度最细,推荐 |
java
// 普通方法 — 锁 this
public synchronized void m1() { ... }
// 静态方法 — 锁 Foo.class
public static synchronized void m2() { ... }
// 代码块 — 锁自定义对象(粒度最小)
private final Object lock = new Object();
public void m3() {
synchronized (lock) { /* 临界区 */ }
// 这里不互斥
}调用同一对象的普通 synchronized 方法和静态 synchronized 方法,不互斥,因为锁对象不同(
thisvsClass)。
为什么 wait() 必须在 synchronized 里?
核心原因:防止丢失唤醒(Lost Wake-up)。synchronized 保证"检查条件"和"进入等待"是原子的。
不加 synchronized(会出问题)
- 线程 A 检查
buffer.isEmpty()→ 为空,准备执行wait() - 此时线程切换,线程 B 抢占 CPU
- 线程 B 执行
buffer.add(data)+notify()→ 信号发出,但 A 还没进入wait(),信号丢失 - 线程 A 回来继续执行
wait()→ 永久等待,再也醒不过来 → 死锁
加了 synchronized(正确)
java
// 消费者
synchronized (lock) {
while (buffer.isEmpty()) { // ① 获取锁,检查条件
lock.wait(); // ② 自动释放锁并挂起
}
// ④ 被唤醒后重新获取锁,继续执行
}
// 生产者
synchronized (lock) {
buffer.add(data); // ③ 此时 A 已在等待,B 拿到锁
lock.notify(); // 唤醒 A
}执行顺序:① → ②(释放锁)→ ③ → ④,notify 一定在 wait 之后,信号不会丢失。
| 不加 synchronized | 加 synchronized | |
|---|---|---|
| 原子性 | 检查与等待之间有缝隙,可被插队 | 检查和等待受锁保护,不可被打断 |
| 信号 | notify 可能在 wait 之前执行,丢失 | notify 必然在 wait 之后,不丢失 |
| 结果 | 线程永久挂起(死锁) | 正常协作 |
不持锁直接调
wait(),JVM 立即抛IllegalMonitorStateException。wait总是和synchronized+while循环一起使用。
volatile 有什么作用?
volatile 解决两个问题,但不保证原子性:
- 可见性:读 volatile 变量直接从主内存读;写后立即刷入主内存,让所有线程看到最新值
- 禁止重排序:插入内存屏障,防止编译器/CPU 把 volatile 前后的指令重排
java
// 经典用法:DCL 单例必须加 volatile
private static volatile Singleton instance;
// 没有 volatile:new Singleton() 可能被重排为
// 1.分配内存 2.返回引用 3.初始化
// 另一线程可能拿到未初始化的对象另一经典用法:线程停止开关 volatile boolean running = true。
volatile 能保证原子性吗?
不能。volatile 只保证可见性和有序性。
能保证的操作:
- 单次读或单次写(含 long/double)
- 简单赋值:
flag = true
不能保证的操作:
- 复合操作:
count++(读-改-写) - 条件更新:
if(a>b) a=b
java
volatile int count = 0;
// 10 线程各执行 1000 次 count++
// 结果 < 10000,volatile 无法解决!
// 正确做法
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 保证原子需要原子性:用
AtomicXxx或synchronized,不要误用 volatile。
i++ 为什么不是线程安全的?
i++ 在字节码层面是三步,不是原子操作:
- 读取:从工作内存读取 i 的当前值
- 计算:在 CPU 寄存器中 +1
- 写回:将结果写回工作内存/主内存
// 线程切换可能发生在任意两步之间
初始 i = 0
线程A 读取 i = 0
线程A 计算 0+1=1 ← 被切换
线程B 读取 i = 0 ← 读到旧值
线程B 计算 0+1=1
线程B 写回 i = 1
线程A 写回 i = 1 ← 覆盖了线程B
期望 i=2,实际 i=1(丢失一次自增)三种正确解法:
- synchronized:三步整体加锁,通用但开销相对大
- AtomicInteger:基于 CAS 无锁操作,高并发下性能好
- LongAdder:分段累加,极高并发比 Atomic 更快