synchronized 关键字浅析
目录
synchronized 是什么
synchronized基于 Java 对象的内置锁(intrinsic lock),也称监视器锁(monitor lock)。每个对象都关联一把内置锁。同一时刻只能有一个线程持有同一把锁。线程释放锁之前的操作,happens-before 另一个线程后续获取同一把锁之后的操作。
因此,面试中分析
synchronized只需先回答一个问题:两段代码锁的是不是同一个对象?
synchronized 的三种用法
| 用法 | 锁对象 |
|---|---|
| 修饰实例方法 | 当前实例,即 this
|
| 修饰静态方法 | 当前类的 Class 对象 |
| 修饰代码块 |
synchronized 括号中的对象 |
实例同步方法
实例同步方法锁的是调用方法的对象,与
synchronized (this)等价。同一实例上的同步方法会互斥,不同实例之间不会。
class Counter {
private int count;
synchronized void increment() {
count++;
}
int get() {
synchronized (this) {
return count;
}
}
}
静态同步方法
静态同步方法锁的是类对象,例如
Counter.class。无论通过哪个实例调用,争用的都是同一把类锁。
class Sequence {
private static int value;
static synchronized int next() {
return ++value;
}
}
同步代码块
同步代码块可以显式指定锁对象,并将临界区缩小到真正需要互斥的代码。
class Counter {
private final Object lock = new Object();
private int count;
void increment() {
synchronized (lock) {
count++;
}
}
}
[!yellow]
专用锁对象通常应声明为private final,避免锁引用被替换,也避免外部代码持有同一把锁。
synchronized 的特性
互斥性
同一把锁保护的临界区不会被多个线程同时执行。因此
count++这类读-改-写复合操作,在所有访问都遵守同一把锁时不会丢失更新。
可见性与有序性
释放某把锁,happens-before 后续获取同一把锁。这保证前一个线程在临界区中的写入,对后一个获取同一把锁的线程可见。
可重入
线程可以重复获取自己已经持有的锁。因此一个同步方法可以调用同一对象的另一个同步方法,不会把自己阻塞。
自动释放锁
同步代码正常结束或异常退出时,JVM 都会自动释放内置锁。这与必须在
finally中手动unlock()的ReentrantLock不同。
对象锁与类锁
| 场景 | 是否互斥 | 原因 |
|---|---|---|
| 同一对象的两个实例同步方法 | 是 | 都锁同一个 this
|
| 不同对象的实例同步方法 | 否 | 两个对象对应两把锁 |
| 同一类的两个静态同步方法 | 是 | 都锁同一个 Class 对象 |
| 实例同步方法与静态同步方法 | 否 | 一个锁实例,一个锁 Class 对象 |
| 同步方法与未同步的普通方法 | 否 | 普通方法不获取锁 |
synchronized 与 volatile
| 对比项 | synchronized |
volatile |
|---|---|---|
| 作用位置 | 方法或代码块 | 成员变量 |
| 互斥性 | 有 | 无 |
| 可见性 | 有 | 有 |
| 适用场景 | 保护多步复合操作或多个关联状态 | 单次读写的状态标记 |
volatile不提供互斥,因此不能单独保证count++这类复合操作的原子性。
synchronized 与 ReentrantLock
| 对比项 | synchronized |
ReentrantLock |
|---|---|---|
| 锁的释放 | JVM 自动释放 | 必须手动 unlock()
|
| 等待可中断 | 不支持中断锁获取 | lockInterruptibly() |
| 尝试获取 | 无超时尝试 API |
tryLock() / tryLock(timeout)
|
| 公平锁 | 无公平性配置 | 可选公平锁或非公平锁 |
| 条件队列 | 每个监视器一个等待集合 | 可以创建多个 Condition
|
默认优先使用
synchronized。只有确实需要可中断、超时、公平性或多条件队列时,再使用ReentrantLock。
底层实现
- 同步代码块编译后使用
monitorenter和monitorexit字节码指令。- 同步方法通过方法的
ACC_SYNCHRONIZED标志表示需要获取监视器。- 锁的具体状态和优化策略属于 JVM 实现细节,不应把某个 JDK 版本的“偏向锁—轻量级锁—重量级锁”实现当作 Java 语言规范。
易错点总结
- 只看到
synchronized就认为一定互斥,忽略了锁对象是否相同。- 用两个实例锁保护同一份静态变量,实际上无法互斥。
- 锁住字符串常量、装箱对象等可能被共享的对象,导致无关代码发生锁竞争。
- 在锁内执行耗时 I/O 或调用不受控的外部方法,使临界区过大。
- 认为
volatile可以代替synchronized保护复合操作。- 多把锁按不同顺序获取,可能造成死锁。