Skip to content

synchronized / volatile

什么是线程安全?

多个线程同时访问同一个对象,不管 JVM 如何调度,程序都能输出正确结果,就叫线程安全。

线程安全的本质是要保证三个核心特性:原子性可见性有序性

不安全示例:多线程同时执行 count++,结果小于预期,因为操作不原子。

安全示例:AtomicIntegerConcurrentHashMap、加锁后的方法。

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 方法,不互斥,因为锁对象不同(this vs Class)。

为什么 wait() 必须在 synchronized 里?

核心原因:防止丢失唤醒(Lost Wake-up)synchronized 保证"检查条件"和"进入等待"是原子的。

不加 synchronized(会出问题)

  1. 线程 A 检查 buffer.isEmpty() → 为空,准备执行 wait()
  2. 此时线程切换,线程 B 抢占 CPU
  3. 线程 B 执行 buffer.add(data) + notify() → 信号发出,但 A 还没进入 wait()信号丢失
  4. 线程 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 立即抛 IllegalMonitorStateExceptionwait 总是和 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 保证原子

需要原子性:用 AtomicXxxsynchronized,不要误用 volatile。

i++ 为什么不是线程安全的?

i++ 在字节码层面是三步,不是原子操作:

  1. 读取:从工作内存读取 i 的当前值
  2. 计算:在 CPU 寄存器中 +1
  3. 写回:将结果写回工作内存/主内存
// 线程切换可能发生在任意两步之间
初始 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 更快