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
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | 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 的区别?
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证(禁止重排序) |
| 阻塞 | 会阻塞 | 不会阻塞 |
Q4: 为什么说 synchronized 是"悲观锁"?
因为它假设一定会发生竞争,进入同步块前必须获取锁。相比之下,CAS 是"乐观锁",假设不会竞争,失败时重试。
小结
- synchronized 三种用法:实例方法、静态方法、代码块
- 对象锁锁实例,类锁锁 Class 对象
- 锁升级:偏向锁 → 轻量级锁 → 重量级锁(不可逆)
- 锁优化:锁消除、锁粗化
- 都是可重入锁
- 优先使用 synchronized,特殊场景用 ReentrantLock