1. 同步问题的本质与synchronized的诞生背景
多线程编程就像是一个繁忙的十字路口,当多个执行流同时访问共享资源时,如果没有合理的调度机制,就会产生数据竞争(Data Race)问题。我在处理金融交易系统时曾遇到过这样的案例:多个线程同时修改账户余额,由于缺乏同步控制,最终余额出现了严重偏差。
synchronized关键字就是Java为解决这类问题提供的原生解决方案。它诞生于JDK 1.0时代,是Java内存模型(JMM)中最重要的同步机制之一。与后来的Lock接口相比,synchronized具有更简洁的语法和自动释放锁的特性,使其成为Java并发编程的基石。
关键认知:synchronized不仅是语法层面的关键字,更是JVM层面的同步原语,它的行为直接影响线程调度和内存可见性。
2. synchronized的三种应用范式
2.1 实例方法同步
在方法声明中添加synchronized关键字,会将整个方法体作为同步块,锁对象是当前实例(this)。这种形式最适合保护对象级别的状态。例如银行账户的转账操作:
java复制public class BankAccount {
private double balance;
public synchronized void transfer(double amount) {
balance += amount; // 这个操作现在具有原子性
}
}
实际开发中需要注意:
- 锁粒度较粗,可能影响并发性能
- 继承场景下,子类方法不会自动继承synchronized修饰
- 不能用于接口方法声明
2.2 静态方法同步
当synchronized修饰静态方法时,锁对象变为类的Class对象。这在需要保护类级别状态时非常有用,比如维护全局计数器:
java复制public class IdGenerator {
private static int counter = 0;
public static synchronized int getNextId() {
return ++counter;
}
}
重要特性:
- 类锁与实例锁互不干扰
- 子类静态方法不会继承synchronized
- 容易成为系统瓶颈(所有线程竞争同一把锁)
2.3 同步代码块
最灵活的用法是指定具体的锁对象,可以精确控制同步范围:
java复制public class FineGrainedSync {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
synchronized(lock1) {
// 临界区代码
}
}
public void methodB() {
synchronized(lock2) {
// 另一个临界区
}
}
}
这种方式的优势在于:
- 可以实现更细粒度的锁控制
- 减少锁竞争提升并发度
- 可以使用任意对象作为锁监视器
3. 底层实现机制深度解析
3.1 字节码层面的实现
通过javap反编译可以看到,synchronized方法会添加ACC_SYNCHRONIZED标志,而同步代码块则会生成monitorenter和monitorexit指令对:
code复制public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
public void syncBlock();
Code:
monitorenter
// 方法体
monitorexit
3.2 对象头与Monitor机制
每个Java对象在堆内存中都包含对象头,其中存储了锁状态信息。synchronized的实现依赖于对象头中的Mark Word和指向Monitor的指针。Monitor(管程)是真正的同步控制器,包含以下关键字段:
- _owner:持有锁的线程
- _EntryList:等待获取锁的线程队列
- _WaitSet:调用wait()后进入等待状态的线程集合
锁升级过程(JDK 6+优化):
- 无锁状态
- 偏向锁(消除无竞争时的同步开销)
- 轻量级锁(通过CAS自旋尝试获取)
- 重量级锁(真正的线程阻塞)
3.3 内存语义保证
synchronized不仅提供互斥访问,还确保内存可见性:
- 进入同步块前,会清空工作内存,从主内存重新加载变量
- 退出同步块时,会把工作内存的修改刷新到主内存
- 遵循happens-before原则,保证操作顺序性
4. 高级特性与最佳实践
4.1 可重入性实现
synchronized具有可重入特性,同一线程可以重复获取已持有的锁。实现原理是在对象头中记录持有线程和进入计数器:
java复制public class ReentrantDemo {
public synchronized void method1() {
method2(); // 可以重入
}
public synchronized void method2() {
// ...
}
}
4.2 锁优化策略
- 减少同步范围:尽量缩小同步代码块
- 降低锁粒度:拆分大锁为多个小锁
- 避免嵌套锁:容易导致死锁
- 使用读写分离:读多写少场景考虑ReadWriteLock
4.3 与wait/notify的配合
经典的生产者-消费者模式实现:
java复制public class BoundedBuffer {
private final Object lock = new Object();
private Queue<Integer> queue = new LinkedList<>();
private int capacity = 10;
public void put(int value) throws InterruptedException {
synchronized(lock) {
while(queue.size() == capacity) {
lock.wait();
}
queue.add(value);
lock.notifyAll();
}
}
public int take() throws InterruptedException {
synchronized(lock) {
while(queue.isEmpty()) {
lock.wait();
}
int value = queue.poll();
lock.notifyAll();
return value;
}
}
}
5. 常见问题排查与性能调优
5.1 死锁诊断与预防
典型死锁场景:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) { ... }
}
// 线程2
synchronized(lockB) {
synchronized(lockA) { ... }
}
排查工具:
- jstack查看线程堆栈
- JConsole/JVisualVM的线程监测
- 第三方工具如Arthas
预防措施:
- 固定锁获取顺序
- 使用tryLock设置超时
- 静态代码分析工具检测
5.2 性能瓶颈定位
使用JFR(Java Flight Recorder)可以分析:
- 锁竞争热点
- 持有时间过长的锁
- 不必要的同步块
优化案例:将StringBuffer(同步)改为StringBuilder(非同步)后,日志处理吞吐量提升40%
5.3 锁膨胀与收缩
通过-XX:+PrintFlagsFinal可以查看JVM锁相关参数:
- BiasedLockingStartupDelay:偏向锁启动延迟
- UseSpinning:是否启用自旋
- PreBlockSpin:自旋次数阈值
在高度竞争场景下,可以考虑禁用偏向锁:
-XX:-UseBiasedLocking
6. 现代Java中的替代方案
虽然synchronized仍是基础,但在JDK 8+中还有其他选择:
- StampedLock:乐观读锁实现
- LongAdder:高并发计数器
- ConcurrentHashMap:并发集合
- CompletableFuture:异步编程
选择建议:
- 简单场景优先用synchronized
- 高竞争场景考虑ReentrantLock
- 读多写少用ReadWriteLock
- 统计计数用LongAdder
在实际项目中,我通常会先用synchronized实现原型,再通过性能测试决定是否需要更复杂的同步机制。过早优化往往是并发问题的根源。