synchronized
synchronized 的基本使用
synchronized 是 Java 中实现线程同步的关键字,它提供了三种基本的使用方式:
1 同步实例方法
public synchronized void method() {
// 同步代码
}
这种方式锁的是当前实例对象(this),哪个对象调用这个方法,哪个对象就是锁对象
比如 user.method() user 就是锁对象
2 同步静态方法
public static synchronized void staticMethod() {
// 同步代码
}
这种方式锁的是当前类的 Class 对象,JVM 级别全局唯一
比如 User.staticMethod() User.class 是锁对象
3 同步代码块
public void method() {
synchronized(obj) {
// 同步代码
}
}
这种方式可以指定锁对象,obj 可以是普通对象也可以是 Class 对象
synchronized 底层原理
synchronized 代码对应的字节码
通过 javap 反编译 synchronized 代码,可以看到以下关键字节码指令:
对于同步方法:
- 方法访问标志中会添加
ACC_SYNCHRONIZED标志
对于同步代码块:
- 使用
monitorenter和monitorexit指令包裹同步代码块
示例:
public void syncMethod() {
synchronized(this) {
System.out.println("Hello");
}
}
对应的字节码:
public void syncMethod();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入同步块
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Hello
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // 正常退出同步块
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // 异常退出同步块
20: aload_2
21: athrow
22: return
监视器锁(Monitor)机制
概念介绍
Java 中每个对象都与一个监视器(Monitor)相关联,synchronized 的实现依赖于这个监视器锁
HotSpot 虚拟机中,监视器存在在对象头的 Mark Word 部分。监视器结构如下:
| 属性 | 含义 | 解释 |
|---|---|---|
| 计数器(_count) | 记录线程重入次数 | 0 表示未被线程持有,大于0表示重入次数 |
| 所有者字段(_owner) | 记录持有锁的线程 | 记录的是线程ID |
| 等待集合(_WaitSet) | 存储等待的线程 | Object.wait() |
| 入口集合(_EntryList) | 存储阻塞等待锁的线程 | 1,获取锁失败 2, Object.notify()、Object.notifyAll() |
- _WaitSet:当线程持有锁时调用
Object.wait()方法,线程会释放锁并进入_WaitSet - _EntryList:当其他线程调用
Object.notify()或Object.notifyAll()方法,_WaitSet中的随机一个或全部转移到_WaitSet中
1. 线程A进入同步块,获取锁
2. 线程B尝试进入同步块,发现锁被A持有,进入 _EntryList
3. 线程A调用 wait(),释放锁并进入 _WaitSet
4. 线程B从 _EntryList 中被选中,获取锁
5. 线程B调用 notify(),线程A从 _WaitSet 移到 _EntryList
6. 线程B退出同步块释放锁
7. 线程A从 _EntryList 中获取锁继续执行
sleep() 不会进入 _WaitSet 或 _EntryList,因为不会释放锁
LockSupport.park() 也不会进入 _WaitSet 或 _EntryList,底层依靠 Unsafe 实现
park():消耗 permit(如果有立即返回,否则就阻塞)
unpark():提供 permit(如果线程已阻塞则唤醒)
AQS 的 Condition.await() 也不会进入 _WaitSet 或 _EntryList,底层依靠条件队列实现
await():进入条件队列
signal():条件队列转入同步队列
获取锁过程
当线程执行到 monitorenter 指令时
- 如果计数器为0,表示锁未被占用,线程获取锁并将计数器置1,设置自己为所有者
- 如果线程已经持有锁,计数器加1(重入)
- 如果锁被其他线程持有,当前线程进入阻塞状态,直到锁被释放
释放锁过程
当线程执行到 monitorexit 指令时
- 计数器减1
- 如果计数器变为0,表示完全释放锁,唤醒等待线程
synchronized 内存语义
- 进入同步块:会清空工作内存,从主内存重新读取共享变量
- 退出同步块:会把工作内存中的修改刷新到主内存
synchronized 总结
- 原子性保证:Monitor 机制
- 可见性保证:JMM 和 MESI,参考 volatile
- 有序性保证:
as-if-serial、happens-before原则和内存屏障,参考 volatile
JVM 对 synchronized 的优化
锁升级
- 无锁状态:初始状态
- 偏向锁:适用于只有一个线程访问同步块的场景
- 在 Mark Word 中记录线程ID
- 同一线程再次进入时不需要任何同步操作
- 轻量级锁:适用于线程交替执行同步块的场景
- 通过 CAS 操作尝试获取锁
- 如果失败,升级为重量级锁
- 重量级锁:适用于多线程竞争激烈的场景
- 使用操作系统的互斥量(mutex)实现
- 线程阻塞和唤醒需要从用户态切换到内核态,开销较大
锁粗化
// 多个同步代码块,要多次获取锁、释放锁
public void method() {
synchronized (this) {
doService1();
}
synchronized (this) {
doService2();
}
synchronized (this) {
doService3();
}
}
// 但是锁对象都是同一个对象,优化后可能成为下面的样子
public void method() {
synchronized (this) {
doService1();
doService2();
doService3();
}
}
锁消除
// 无用的锁,因为每次调用这个方法,都会创建一个新的 user 对象,所以每次的锁对象都不一样,其实完全没必要加锁
public void method2() {
User user = new User()
synchronized(user) {
doService();
}
}
// 优化后
public void method2() {
doService();
}

浙公网安备 33010602011771号