面试八股之从jvm层面深入解析Java中的synchronized关键字
一、synchronized概述
synchronized
是Java中最基本的同步机制,用于控制多个线程对共享资源的访问,确保同一时刻只有一个线程可以执行特定代码段或访问特定对象。它是Java内置的互斥锁实现,能够有效解决多线程环境下的原子性、可见性和有序性问题。
基本作用
- 原子性:确保互斥操作,防止多个线程同时执行临界区代码
- 可见性:保证锁释放前对共享变量的修改对其他线程可见
- 有序性:防止指令重排序,确保代码执行顺序符合预期
二、synchronized的三种使用方式
1. 同步实例方法
public class Counter {private int count = 0;public synchronized void increment() {count++;}
}
- 锁对象:当前实例对象(this)
- 作用范围:整个方法体
2. 同步静态方法
public class StaticCounter {private static int count = 0;public static synchronized void increment() {count++;}
}
- 锁对象:当前类的Class对象(StaticCounter.class)
- 作用范围:整个静态方法体
3. 同步代码块
public class BlockCounter {private int count = 0;private final Object lock = new Object();public void increment() {synchronized(lock) {count++;}}
}
- 锁对象:可以是任意对象实例
- 作用范围:代码块内部
- 灵活性高,可以精确控制同步范围
三、JVM层面的实现原理
1. 对象头与Mark Word
在HotSpot虚拟机中,Java对象在内存中的布局分为三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
其中对象头包含两部分:
- Mark Word:存储对象的hashCode、GC分代年龄、锁状态等信息
- 类型指针:指向类元数据的指针
在32位JVM中,Mark Word结构如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
2. 锁升级过程
JDK1.6之后,synchronized进行了重要优化,引入了锁升级机制,而不是直接使用重量级锁。锁的状态会随着竞争情况从低到高逐步升级:
- 无锁状态:新创建的对象处于无锁状态
- 偏向锁:适用于只有一个线程访问同步块的场景
- 轻量级锁:当有少量线程竞争时,通过CAS操作获取锁
- 重量级锁:当竞争激烈时,升级为操作系统层面的互斥量
偏向锁(Biased Locking)
- 目的:减少无竞争情况下的同步开销
- 原理:在Mark Word中记录偏向线程ID
- 优点:加锁解锁不需要额外操作
- 适用场景:单线程访问同步块
轻量级锁(Lightweight Locking)
- 目的:减少多线程交替执行同步块时的性能消耗
- 原理:使用CAS操作将Mark Word替换为指向线程栈中锁记录的指针
- 优点:避免线程阻塞
- 缺点:自旋会消耗CPU
重量级锁(Heavyweight Locking)
- 目的:处理高竞争情况
- 原理:通过操作系统的互斥量(mutex)实现
- 特点:线程会阻塞,性能开销大
3. 字节码层面分析
编译后的同步代码块会在字节码中使用monitorenter
和monitorexit
指令实现:
public void syncMethod();Code:0: aload_01: dup2: astore_13: monitorenter // 进入同步块4: aload_15: monitorexit // 正常退出同步块6: goto 149: astore_210: aload_111: monitorexit // 异常退出同步块12: aload_213: athrow14: return
可以看到编译器会自动生成异常处理逻辑,确保锁在异常情况下也能被释放。
四、锁优化技术
1. 自旋锁与自适应自旋
- 自旋锁:线程不立即阻塞,而是执行忙循环(自旋)等待锁释放
- 自适应自旋:JVM根据之前自旋等待的成功率动态调整自旋时间
2. 锁消除(Lock Elimination)
JIT编译器通过逃逸分析,发现某些锁对象不可能被共享时,会消除这些锁操作。
public String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();
}
在这个例子中,StringBuffer是局部变量,不会被其他线程访问,JVM会消除其内部同步操作。
3. 锁粗化(Lock Coarsening)
将多个连续的锁操作合并为一个更大的锁操作,减少频繁同步带来的性能损耗。
public void method() {synchronized(lock) {// 操作1}synchronized(lock) {// 操作2}// 可能被优化为synchronized(lock) {// 操作1// 操作2}
}
五、性能考量与最佳实践
1. 性能比较
- 无竞争:偏向锁 > 轻量级锁 > 重量级锁
- 低竞争:轻量级锁 > 偏向锁 > 重量级锁
- 高竞争:重量级锁更合适
2. 使用建议
- 减小同步范围:只在必要的地方加锁
- 降低锁粒度:使用多个锁控制不同资源
- 避免锁嵌套:容易导致死锁
- 考虑替代方案:在适当场景使用
java.util.concurrent
包中的并发工具
3. 示例:双重检查锁定(Double-Checked Locking)
public class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
注意:必须使用volatile
关键字防止指令重排序问题。
六、总结
synchronized
关键字是Java并发编程的基础构建块,从JDK1.0开始就存在,经过多次优化(尤其是JDK1.6的锁升级机制)后,性能已经大幅提升。理解其JVM层面的实现原理,有助于我们编写更高效、更安全的并发程序。
在实际开发中,应根据具体场景选择合适的同步策略,对于简单同步需求,synchronized
仍然是一个简单有效的选择;对于更复杂的并发场景,可以考虑java.util.concurrent
包中更高级的并发工具。