1. Java synchronized锁机制概述
在Java多线程编程中,synchronized关键字是最基础的线程同步工具。我第一次接触这个概念是在处理一个电商库存扣减的业务场景时,当时遇到了严重的超卖问题。通过深入理解synchronized的工作原理,最终解决了这个并发难题。
synchronized本质上是一种互斥锁,它能够确保同一时刻只有一个线程可以执行被保护的代码块或方法。这种机制对于保护共享资源、避免竞态条件至关重要。与后来的Lock接口相比,synchronized作为JVM内置的同步机制,使用更简单且不会出现忘记释放锁的情况。
重要提示:虽然synchronized使用简单,但不恰当的使用会导致性能问题。我曾见过一个案例,有人在整个Service层方法上加synchronized,结果系统吞吐量直接下降了80%。
2. synchronized的三种应用方式
2.1 实例方法同步
这是最常见的用法,锁的是当前对象实例:
java复制public synchronized void method() {
// 线程安全代码
}
这种写法等价于:
java复制public void method() {
synchronized(this) {
// 线程安全代码
}
}
在实际项目中,我通常只在方法内部真正需要同步的代码块上加锁,而不是整个方法。这样可以减少锁的持有时间,提高并发性能。
2.2 静态方法同步
当synchronized修饰静态方法时,锁的是类的Class对象:
java复制public static synchronized void staticMethod() {
// 线程安全代码
}
这相当于:
java复制public static void staticMethod() {
synchronized(MyClass.class) {
// 线程安全代码
}
}
我曾经在一个工具类中错误地混用了实例同步和静态同步,导致出现了难以发现的死锁问题。这个教训让我明白:不同类型的锁一定要明确区分。
2.3 代码块同步
最灵活的用法是指定锁对象:
java复制public void method() {
// 非同步代码
synchronized(lockObject) {
// 同步代码
}
// 非同步代码
}
这里lockObject可以是任意对象实例。在项目中,我通常会专门创建一个final对象作为锁:
java复制private final Object lock = new Object();
这样可以避免意外使用其他对象作为锁带来的风险。
3. synchronized的底层实现原理
3.1 字节码层面分析
通过javap反编译工具,我们可以看到synchronized在字节码中的真实面貌。以这段代码为例:
java复制public void syncMethod() {
synchronized(this) {
System.out.println("Hello");
}
}
编译后的字节码会包含以下关键指令:
code复制monitorenter // 获取锁
// 同步代码
monitorexit // 正常释放锁
monitorexit // 异常时释放锁
JVM会确保无论同步代码块是正常结束还是抛出异常,锁都会被正确释放。这个设计避免了锁泄漏的问题。
3.2 对象头与Mark Word
每个Java对象在内存中都由三部分组成:
- 对象头
- 实例数据
- 对齐填充
其中对象头又包含:
- Mark Word(存储对象运行时数据)
- 类型指针(指向类元数据)
在64位JVM中,Mark Word的结构如下:
| 锁状态 | 存储内容 |
|---|---|
| 无锁 | hashCode(31bit)+分代年龄(4bit)等 |
| 偏向锁 | 线程ID(54bit)+时间戳(2bit) |
| 轻量级锁 | 指向栈中锁记录的指针(62bit) |
| 重量级锁 | 指向互斥量的指针(62bit) |
| GC标记 | 空 |
这个结构会根据锁状态动态变化。通过Java对象布局工具(JOL)可以直观查看:
java复制System.out.println(ClassLayout.parseInstance(obj).toPrintable());
3.3 锁升级全过程
现代JVM(如HotSpot)采用逐步升级的锁机制来优化性能:
- 无锁状态:新创建的对象初始状态
- 偏向锁:当第一个线程访问时,JVM会将锁偏向该线程
- 优点:后续该线程获取锁无需同步操作
- 缺点:当有其他线程竞争时需要撤销偏向锁
- 轻量级锁:当发生轻度竞争时,JVM会升级为轻量级锁
- 通过CAS操作获取锁
- 失败时会进行短暂的自旋尝试
- 重量级锁:当竞争激烈时,最终会升级为重量级锁
- 涉及操作系统互斥量
- 线程会进入阻塞状态
这个升级过程是不可逆的。在我的性能调优实践中,发现理解这个过程对于诊断并发瓶颈非常有帮助。
4. 锁优化实践技巧
4.1 减小锁粒度
一个常见的错误是锁的粒度过大。比如:
java复制// 不推荐
public synchronized void processOrder(Order order) {
// 处理订单的多个步骤
}
更好的做法是:
java复制public void processOrder(Order order) {
synchronized(order) {
// 只同步必要的部分
}
}
我曾经通过细化锁粒度将一个接口的TPS从200提升到了1500。
4.2 避免死锁
死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
预防死锁的建议:
- 按固定顺序获取锁
- 使用tryLock设置超时
- 避免在持有锁时调用外部方法
4.3 锁分离技术
对于读写比例高的场景,可以使用ReadWriteLock:
java复制ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
void read() {
rwLock.readLock().lock();
try {
// 读操作
} finally {
rwLock.readLock().unlock();
}
}
void write() {
rwLock.writeLock().lock();
try {
// 写操作
} finally {
rwLock.writeLock().unlock();
}
}
5. 性能对比与监控
5.1 synchronized vs Lock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | Java代码实现 |
| 锁获取 | 自动释放 | 必须手动unlock |
| 可中断 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可配置 |
| 条件变量 | 有限 | 更灵活 |
5.2 监控锁状态
推荐使用以下工具监控锁情况:
- JConsole:可视化查看线程和锁状态
- VisualVM:更强大的分析功能
- Arthas:线上诊断神器
我曾经用Arthas诊断过一个线上死锁问题,命令如下:
bash复制thread -b # 查找死锁
monitor -c 5 com.example.Service method # 监控方法调用
6. 常见问题排查
6.1 锁竞争严重
症状:
- 线程大量处于BLOCKED状态
- CPU使用率不高但吞吐量低
解决方案:
- 使用JStack分析线程栈
- 考虑减小锁粒度或使用并发容器
6.2 锁升级导致性能下降
症状:
- 初期性能良好,随着并发增加性能骤降
解决方案:
- 使用JOL分析对象头变化
- 考虑使用并发度更高的数据结构
6.3 错误使用类锁
一个典型错误:
java复制public class Util {
private static Map<String, String> cache = new HashMap<>();
public static synchronized void put(String k, String v) {
cache.put(k, v);
}
}
这样会导致所有调用方共用一个锁。更好的做法是使用ConcurrentHashMap。
7. 实际案例分析
7.1 电商库存扣减
最初实现:
java复制public synchronized void deductStock(Long itemId, int num) {
// 查询库存
// 检查是否充足
// 扣减库存
}
问题:所有商品共用一个锁,并发度极低
优化方案:
java复制private static Map<Long, Object> itemLocks = new ConcurrentHashMap<>();
public void deductStock(Long itemId, int num) {
Object lock = itemLocks.computeIfAbsent(itemId, k -> new Object());
synchronized(lock) {
// 扣减逻辑
}
}
7.2 多阶段任务同步
场景:需要多个线程完成第一阶段后才能开始第二阶段
解决方案:
java复制public class PhaseControl {
private final Object phase1Lock = new Object();
private final Object phase2Lock = new Object();
private volatile boolean phase1Done = false;
public void phase1() {
synchronized(phase1Lock) {
// 阶段1工作
phase1Done = true;
phase1Lock.notifyAll();
}
}
public void phase2() {
synchronized(phase1Lock) {
while(!phase1Done) {
phase1Lock.wait();
}
}
synchronized(phase2Lock) {
// 阶段2工作
}
}
}
8. 高级话题探讨
8.1 锁消除与锁粗化
JVM会进行两种锁优化:
-
锁消除:当JVM检测到不可能存在共享数据竞争时,会消除锁
java复制public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }在这个例子中,StringBuffer的同步操作会被消除
-
锁粗化:当检测到连续多个细粒度锁操作时,会合并为一个更大的锁
java复制for(int i=0; i<100; i++) { synchronized(this) { // 小操作 } }可能被优化为:
java复制synchronized(this) { for(int i=0; i<100; i++) { // 小操作 } }
8.2 偏向锁的争议
虽然偏向锁可以减少无竞争时的同步开销,但在高竞争环境下,偏向锁的撤销成本很高。因此,在Java 15中,偏向锁被标记为废弃(JEP 374),并在Java 21中被完全移除。
在我的基准测试中,对于竞争激烈的场景,禁用偏向锁确实能提升性能:
bash复制-XX:-UseBiasedLocking
9. 最佳实践总结
经过多年实践,我总结了以下synchronized使用原则:
- 明确锁范围:只同步必要的代码块
- 明确锁对象:使用专用对象而非this或类对象
- 控制锁时长:避免在同步块中执行耗时操作
- 避免嵌套锁:容易导致死锁
- 优先使用并发容器:如ConcurrentHashMap
- 考虑替代方案:如volatile、原子类等
对于大多数应用,合理使用synchronized已经能满足并发需求。但在极高并发的场景下,可能需要考虑更高级的并发工具。