线程安全与synchronized

一、线程安全基础:竞态条件 & 临界区

1. 竞态条件(Race Condition)

多个线程同时操作、修改同一个共享变量,执行结果依赖线程执行的先后顺序,最终导致结果错误。

  • 根本原因:读 - 改 - 写三步不是原子操作。
  • 典型例子:i++ 多线程下计数错误。

2. 临界区(Critical Section)

会发生线程安全问题的代码块,就是临界区。

  • 多个线程同时进入临界区 → 线程不安全。
  • 同一时间只允许一个线程进入 → 线程安全。

二、synchronized 三种使用方式

synchronized独占互斥锁,保证同一时刻只有一个线程进入临界区。

1. 修饰实例方法(锁当前对象 this)

1
2
3
public synchronized void method() {
// 临界区
}

锁对象:this(当前实例对象)

2. 修饰静态方法(锁当前类 Class 对象)

1
2
3
public static synchronized void staticMethod() {
// 临界区
}

锁对象:当前类.class(全局唯一)

3. 修饰代码块(灵活指定锁对象)

1
2
3
synchronized (锁对象) {
// 临界区
}

锁对象:可以是任意对象(推荐用 private final Object lock = new Object()


三、synchronized 三大锁特性

1. 互斥性(Mutex)

同一时刻只有一个线程能持有锁,其他线程阻塞等待。

→ 保证原子性

2. 可重入性(Reentrant)

同一个线程可以再次获取自己已经持有的锁,不会死锁。

  • 底层:锁关联计数器,获取 + 1,释放 - 1,为 0 才真正释放。
  • 作用:避免同一个线程多次调用同步方法时死锁。

3. 内存语义(Memory Semantics)

保证:

  • 可见性:解锁前把变量刷回主内存,加锁时重新读取主内存。
  • 有序性:禁止临界区内外代码重排。

临界区内部可以重排;临界区外部代码,不能重排到临界区内部;临界区内部代码,也不能重排到外部。

  • 原子性:整块代码串行执行。

四、锁升级流程(重点!面试必考)

JDK 1.6 以后为了优化性能,synchronized 不再一上来就是重量级锁,而是逐步升级

1
无锁 → 偏向锁 → 轻量级锁 → 重量级锁

只会升级,不会降级

1. 偏向锁(Biased Lock)

  • 场景只有一个线程反复获取锁,无竞争。
  • 原理:锁对象标记当前线程 ID,下次直接进,无 CAS 操作
  • 特点:开销极低。

2. 轻量级锁(Lightweight Lock)

  • 场景:少量线程竞争,交替获取锁(无长时间阻塞)。
  • 原理:使用CAS + 自旋尝试获取锁,不阻塞线程。
  • 特点:避免线程阻塞 / 唤醒的开销,消耗 CPU。

3. 重量级锁(Heavyweight Lock)

  • 场景:高竞争、线程长时间占用锁。
  • 原理:内核态互斥锁,线程阻塞进入等待队列。
  • 特点:不消耗 CPU,但线程切换开销大。

五、synchronized 底层:Monitor 机制

1. 什么是 Monitor?

Monitor 叫管程 / 监视器,是 JVM 底层实现的同步机制(C++ 实现)。

每个对象天生关联一个 Monitor

2. 核心结构

  • _owner:持有锁的线程
  • _WaitSet:wait () 等待队列
  • _EntryList:阻塞等待队列

3. 加锁 / 解锁流程

  1. 线程进入同步块 → 尝试获取 Monitor。
  2. 若未被持有 → _owner 指向当前线程,计数器 + 1。
  3. 若已被持有 → 进入 _EntryList 阻塞
  4. 线程 wait () → 释放锁,进入 _WaitSet。
  5. 执行完毕 → 释放锁,唤醒等待线程。

4. 底层指令

  • 加锁:monitorenter
  • 解锁:monitorexit(自动生成,异常也会保证解锁)

线程安全与synchronized
http://hanqichuan.com/2026/04/15/java并发/线程安全与synchronized/
作者
韩启川
发布于
2026年4月15日
许可协议