1. synchronized同步机制的核心概念
在多线程编程中,synchronized是Java语言提供的一种内置锁机制,用于解决多线程环境下的数据竞争问题。它的本质是在对象头中设置一个标记位,通过这个标记位来控制线程对共享资源的访问。
synchronized关键字可以作用于三个不同的位置:
- 实例方法:锁住当前对象实例
- 静态方法:锁住当前类的Class对象
- 代码块:可以指定锁对象
重要提示:synchronized是Java中实现线程安全的最基本手段,但过度使用会导致性能下降。在实际开发中需要根据场景权衡安全性和性能。
2. synchronized的实现原理
2.1 对象头与Monitor机制
每个Java对象在内存中都由三部分组成:
- 对象头
- 实例数据
- 对齐填充
其中对象头包含了两部分重要信息:
- Mark Word:存储对象的hashCode、GC分代年龄、锁状态等
- Klass Pointer:指向对象元数据的指针
当使用synchronized时,JVM会通过对象头中的Mark Word来实现锁机制。具体来说:
code复制|-------------------------------------------------------|
| Mark Word (64 bits) |
|-------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
|-------------------------------------------------------|
锁状态通过最后2位的lock标志位来表示:
- 00:轻量级锁
- 01:无锁/偏向锁
- 10:重量级锁
- 11:GC标记
2.2 锁升级过程
JVM为了优化synchronized的性能,设计了锁升级机制:
- 偏向锁:当第一个线程访问时,会在对象头和栈帧中记录线程ID
- 轻量级锁:当有竞争时,会通过CAS操作尝试获取锁
- 重量级锁:竞争激烈时,会升级为操作系统级别的互斥量
这种锁升级策略是JVM的优化手段,目的是减少锁操作的开销。
3. synchronized的三种使用方式
3.1 同步实例方法
java复制public synchronized void method() {
// 同步代码
}
这种写法等价于:
java复制public void method() {
synchronized(this) {
// 同步代码
}
}
3.2 同步静态方法
java复制public static synchronized void staticMethod() {
// 同步代码
}
这种写法等价于:
java复制public static void staticMethod() {
synchronized(MyClass.class) {
// 同步代码
}
}
3.3 同步代码块
java复制public void method() {
// 非同步代码
synchronized(obj) {
// 同步代码
}
// 非同步代码
}
这种方式更加灵活,可以精确控制同步范围。
4. synchronized的底层实现细节
4.1 Monitor机制
synchronized的实现依赖于Monitor对象,每个Java对象都关联一个Monitor。Monitor包含以下关键组件:
- _owner:指向持有锁的线程
- _EntryList:存放等待锁的线程
- _WaitSet:存放调用了wait()的线程
当线程尝试获取锁时:
- 如果锁未被占用,则获取锁成功
- 如果锁已被占用,则进入_EntryList等待
- 当锁释放时,会从_EntryList中唤醒一个线程
4.2 字节码层面分析
对于同步代码块,编译器会生成monitorenter和monitorexit指令:
java复制public void test() {
synchronized(obj) {
System.out.println("hello");
}
}
对应的字节码:
code复制public void test();
Code:
0: aload_1
1: dup
2: astore_2
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_2
13: monitorexit // 正常退出同步块
14: goto 22
17: astore_3
18: aload_2
19: monitorexit // 异常退出同步块
20: aload_3
21: athrow
22: return
可以看到编译器会自动处理异常情况,确保锁能被正确释放。
5. synchronized的性能优化
5.1 锁消除
JVM在JIT编译时,如果发现不可能存在共享数据竞争,就会消除锁。例如:
java复制public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
StringBuffer是线程安全的,但在这个方法中sb是局部变量,不会被其他线程访问,所以JVM会消除锁。
5.2 锁粗化
当JVM检测到一连串操作都对同一个对象加锁时,会把锁的范围扩大到整个操作序列。例如:
java复制public void method() {
synchronized(obj) {
// 操作1
}
synchronized(obj) {
// 操作2
}
synchronized(obj) {
// 操作3
}
}
可能会被优化为:
java复制public void method() {
synchronized(obj) {
// 操作1
// 操作2
// 操作3
}
}
5.3 偏向锁与轻量级锁
偏向锁和轻量级锁都是JVM为了减少同步开销而设计的优化:
| 锁类型 | 适用场景 | 实现方式 | 优点 |
|---|---|---|---|
| 偏向锁 | 只有一个线程访问 | 记录线程ID | 加解锁无额外开销 |
| 轻量级锁 | 多个线程交替访问 | CAS操作 | 避免线程阻塞 |
| 重量级锁 | 多线程竞争 | 操作系统互斥量 | 保证线程安全 |
6. synchronized的常见问题与解决方案
6.1 死锁问题
死锁是指两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行。例如:
java复制// 线程1
synchronized(obj1) {
synchronized(obj2) {
// ...
}
}
// 线程2
synchronized(obj2) {
synchronized(obj1) {
// ...
}
}
解决方案:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 使用更高级的并发工具
6.2 性能问题
synchronized在竞争激烈时性能会下降,因为会升级为重量级锁,导致线程上下文切换。
优化建议:
- 减小同步代码块的范围
- 使用读写锁(ReadWriteLock)替代
- 考虑使用并发容器
- 使用CAS操作(Atomic类)
6.3 可见性问题
synchronized不仅能保证原子性,还能保证可见性。因为:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中的共享变量值,从主内存重新读取
7. synchronized与其他同步机制对比
7.1 synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | JDK实现 |
| 锁获取 | 自动获取释放 | 需要手动lock/unlock |
| 可中断 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可配置 |
| 条件变量 | 一个 | 多个 |
| 性能 | 优化后差距不大 | 高竞争时更好 |
7.2 synchronized vs volatile
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 使用场景 | 代码块/方法 | 变量 |
7.3 synchronized vs CAS
| 特性 | synchronized | CAS |
|---|---|---|
| 实现方式 | 锁 | 无锁 |
| 阻塞 | 会阻塞 | 不会 |
| 适用场景 | 复杂同步 | 简单原子操作 |
| 性能 | 高竞争时差 | 高竞争时好 |
8. 实际应用中的最佳实践
- 同步范围最小化:只同步必要的代码块
- 避免嵌套锁:容易导致死锁
- 使用私有锁:避免外部干扰
java复制private final Object lock = new Object(); public void method() { synchronized(lock) { // ... } } - 考虑并发容器:如ConcurrentHashMap
- 读写分离:读多写少时使用ReadWriteLock
- 监控锁竞争:使用JConsole或VisualVM
9. JVM对synchronized的优化
现代JVM对synchronized做了大量优化:
- 自适应自旋:根据历史数据动态调整自旋次数
- 锁消除:消除不可能存在竞争的锁
- 锁粗化:合并连续的锁操作
- 偏向锁:优化无竞争情况下的性能
- 轻量级锁:使用CAS避免线程阻塞
这些优化使得在大多数情况下,synchronized的性能已经足够好。
10. 常见面试问题解析
-
synchronized的实现原理?
- 通过对象头中的Mark Word实现
- 包含偏向锁、轻量级锁、重量级锁三种状态
- 依赖Monitor对象管理线程
-
synchronized和ReentrantLock的区别?
- synchronized是关键字,ReentrantLock是类
- ReentrantLock功能更丰富(可中断、公平锁等)
- synchronized由JVM优化,ReentrantLock需要手动释放
-
synchronized能否保证可见性?
- 能,因为JMM规定解锁前必须刷新变量到主内存
- 加锁时会从主内存重新读取变量值
-
什么是锁升级?
- 从偏向锁→轻量级锁→重量级锁的过程
- 是JVM为了优化性能设计的机制
-
如何避免死锁?
- 按固定顺序获取锁
- 使用tryLock设置超时
- 减少锁的持有时间
