一、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_0
1: dup
2: astore_1
3: monitorenter // 进入同步块
4: aload_1
5: monitorexit // 正常退出同步块
6: goto 14
9: astore_2
10: aload_1
11: monitorexit // 异常退出同步块
12: aload_2
13: athrow
14: 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
包中更高级的并发工具。