知识模块
☕ Java 知识模块
三、Java 并发编程
synchronized 关键字

synchronized 关键字

synchronized 是 Java 内置的线程同步机制,用于保证多线程环境下的线程安全。

基本用法

三种使用方式

// 1. 同步实例方法 - 锁的是 this 对象
public synchronized void instanceMethod() {
    // 临界区代码
}
 
// 2. 同步静态方法 - 锁的是 Class 对象
public static synchronized void staticMethod() {
    // 临界区代码
}
 
// 3. 同步代码块 - 锁的是指定对象
public void blockMethod() {
    synchronized (this) {  // 或任意对象
        // 临界区代码
    }
}

对象锁 vs 类锁

类型锁对象作用范围
对象锁实例对象同一实例的同步方法互斥
类锁Class 对象所有实例的静态同步方法互斥
public class SyncDemo {
    // 对象锁 - 不同实例不互斥
    public synchronized void methodA() {}
    
    // 类锁 - 所有实例互斥
    public static synchronized void methodB() {}
    
    // 等价于类锁
    public void methodC() {
        synchronized (SyncDemo.class) {}
    }
}

锁升级(JDK 6+)

JVM 通过锁升级优化 synchronized 性能,升级顺序:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

偏向锁

场景:同一个线程多次获取同一把锁

原理:在对象头中记录线程 ID,后续该线程进入同步块时,无需加锁

// 第一次获取锁:CAS 修改对象头中的线程 ID
// 后续进入:检查线程 ID,相同则直接进入

撤销:当其他线程竞争时,偏向锁撤销,升级为轻量级锁

轻量级锁

场景:多个线程交替执行同步块(几乎没有竞争)

原理:使用 CAS 操作将锁记录(Lock Record)存储在栈帧中

线程栈帧                    对象头
┌─────────────┐            ┌─────────────┐
│ Lock Record │ ──CAS──→   │ Mark Word   │
│ (锁记录)    │            │ (存储锁记录地址) │
└─────────────┘            └─────────────┘

自旋:获取失败时自旋等待(自适应自旋),避免阻塞

重量级锁

场景:竞争激烈

原理:依赖操作系统的互斥量(Mutex),线程阻塞

代价:用户态 → 内核态切换,开销较大

锁升级流程

无锁
  ↓ (单线程首次访问)
偏向锁 (记录线程ID)
  ↓ (其他线程竞争)
轻量级锁 (CAS + 自旋)
  ↓ (自旋失败/竞争激烈)
重量级锁 (Mutex 阻塞)

锁优化

锁消除

JIT 检测到不可能被共享的对象,自动消除锁:

public String concat(String s1, String s2) {
    StringBuffer sb = new StringBuffer();  // sb 是局部变量,不会被逃逸
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
    // StringBuffer 的 append 是同步方法,但 sb 不会逃逸
    // JIT 会消除这里的锁
}

锁粗化

对多次连续加锁/解锁操作合并为一次:

// 优化前:循环内加锁
for (int i = 0; i < 100; i++) {
    synchronized (lock) {
        list.add(i);
    }
}
 
// 优化后:锁粗化到循环外
synchronized (lock) {
    for (int i = 0; i < 100; i++) {
        list.add(i);
    }
}

synchronized vs ReentrantLock

特性synchronizedReentrantLock
实现方式JVM 内置JDK API
锁获取自动获取/释放手动 lock()/unlock()
可中断lockInterruptibly()
公平性非公平可选公平/非公平
条件变量单一多个 Condition
读写锁不支持ReentrantReadWriteLock
性能JDK 6 后优化,差距不大高竞争时略优

使用选择

优先使用 synchronized

  • 代码简洁
  • 自动释放锁,不会忘记 unlock
  • JVM 优化较好

使用 ReentrantLock

  • 需要公平锁
  • 需要可中断的锁获取
  • 需要多个条件变量
  • 需要尝试获取锁(tryLock)
// ReentrantLock 使用示例
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
 
lock.lock();
try {
    while (条件不满足) {
        condition.await();
    }
    // 执行逻辑
    condition.signalAll();
} finally {
    lock.unlock();  // 必须在 finally 中释放
}

可重入性

synchronized 和 ReentrantLock 都是可重入锁:

public class ReentrantDemo {
    public synchronized void methodA() {
        methodB();  // 可以再次获取同一把锁
    }
    
    public synchronized void methodB() {
        // 正常执行
    }
}

原理:锁计数器。每次获取锁 +1,每次释放 -1,为 0 时真正释放。

常见面试题

Q1: synchronized 锁的是什么?

  • 实例方法:锁的是当前实例对象(this)
  • 静态方法:锁的是 Class 对象
  • 代码块:锁的是括号内指定的对象

Q2: 锁升级可以降级吗?

不可以。锁只能升级,不能降级。这是 JVM 的设计决策。

Q3: synchronized 和 volatile 的区别?

特性synchronizedvolatile
原子性保证不保证
可见性保证保证
有序性保证保证(禁止重排序)
阻塞会阻塞不会阻塞

Q4: 为什么说 synchronized 是"悲观锁"?

因为它假设一定会发生竞争,进入同步块前必须获取锁。相比之下,CAS 是"乐观锁",假设不会竞争,失败时重试。

小结

  • synchronized 三种用法:实例方法、静态方法、代码块
  • 对象锁锁实例,类锁锁 Class 对象
  • 锁升级:偏向锁 → 轻量级锁 → 重量级锁(不可逆)
  • 锁优化:锁消除、锁粗化
  • 都是可重入锁
  • 优先使用 synchronized,特殊场景用 ReentrantLock