1. synchronized关键字深度解析
在多线程编程的世界里,synchronized就像交通信号灯,控制着线程对共享资源的访问秩序。这个看似简单的关键字背后,隐藏着JVM级别的复杂机制。我第一次真正理解它的重要性是在一个电商秒杀项目中,当并发请求达到峰值时,没有正确使用synchronized导致的库存超卖问题让我们付出了惨痛代价。
synchronized是Java中最基础的线程同步工具,它通过内置锁(Monitor)机制实现对代码块或方法的互斥访问。不同于后来的Lock接口实现,synchronized是JVM原生支持的同步方案,它的使用更简单,但性能优化空间也更小。对于日常开发中80%的同步场景,正确使用synchronized就足以应对。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. synchronized的实现原理
2.1 对象头与Monitor机制
每个Java对象在内存中都由对象头、实例数据和对齐填充三部分组成。对象头中的Mark Word字段(32位系统占4字节,64位系统占8字节)就是synchronized实现锁机制的关键所在。当对象被synchronized修饰时,Mark Word会存储指向Monitor对象的指针。
Monitor(管程)是JVM层面的同步原语,每个Java对象都关联一个Monitor。这个Monitor包含以下几个关键字段:
- _owner:记录持有锁的线程
- _EntryList:存放等待获取锁的线程
- _WaitSet:存放调用wait()后等待的线程
注意:在JDK 6之前,synchronized是重量级锁,直接对应操作系统层面的互斥量(mutex),线程阻塞和唤醒需要切换到内核态,性能开销很大。
2.2 锁升级优化过程
JDK 6引入了偏向锁、轻量级锁等优化,形成了现在的锁升级路径:
- 无锁状态:新创建的对象默认处于无锁状态
- 偏向锁(Biased Locking):当第一个线程访问时,JVM通过CAS操作将线程ID记录到Mark Word
- 轻量级锁:当有第二个线程尝试获取锁时,升级为轻量级锁,通过自旋(Spin)尝试获取
- 重量级锁:自旋超过阈值(默认10次)后升级为重量级锁,线程进入阻塞状态
锁升级是不可逆的过程,这种设计是为了在无竞争或低竞争场景下减少同步开销。通过以下命令可以查看对象头信息:
bash复制# 添加JVM参数
-XX:+PrintFlagsFinal | grep BiasedLocking
3. synchronized的四种使用方式
3.1 实例方法同步
java复制public synchronized void method() {
// 同步代码
}
这种写法锁的是当前实例对象(this),不同实例之间不会相互阻塞。适合需要保护实例变量的场景,比如:
java复制public class Counter {
private int count;
public synchronized void increment() {
count++;
}
}
3.2 静态方法同步
java复制public static synchronized void staticMethod() {
// 同步代码
}
锁的是当前类的Class对象(如Counter.class),会阻塞所有调用该静态方法的线程。适合保护类级别共享资源,比如数据库连接池的初始化。
3.3 同步代码块-对象锁
java复制public void method() {
synchronized(this) {
// 同步代码
}
}
与同步实例方法等效,但可以更精确控制同步范围。实际开发中更推荐这种写法,因为可以缩小同步区域,提升并发性能。
3.4 同步代码块-自定义锁
java复制private final Object lock = new Object();
public void method() {
synchronized(lock) {
// 同步代码
}
}
使用专门的对象作为锁,可以避免意外锁住其他不相关的同步块。这是最灵活的同步方式,也是《Java并发编程实战》推荐的做法。
4. synchronized的典型应用场景
4.1 单例模式实现
双重检查锁定(DCL)是最经典的synchronized应用场景:
java复制public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里需要注意:
- 必须使用volatile防止指令重排序
- 同步块内需要再次检查(避免多个线程同时通过第一层检查)
4.2 线程安全计数器
java复制public class SafeCounter {
private int value;
public synchronized int getAndIncrement() {
return value++;
}
public synchronized int get() {
return value;
}
}
对于简单的计数器场景,synchronized的性能已经足够。但在超高并发下(如QPS>10万),可以考虑AtomicLong等原子类。
4.3 资源池管理
数据库连接池的获取和释放必须保证线程安全:
java复制public class ConnectionPool {
private LinkedList<Connection> pool = new LinkedList<>();
public synchronized Connection getConnection() {
while (pool.isEmpty()) {
wait();
}
return pool.removeFirst();
}
public synchronized void release(Connection conn) {
pool.addLast(conn);
notifyAll();
}
}
这里展示了synchronized与wait()/notify()的配合使用,实现线程间协作。
5. 性能优化与最佳实践
5.1 减小同步范围
错误的写法:
java复制public synchronized void process() {
// 耗时IO操作(不需要同步)
readFile();
// 需要同步的操作
updateCount();
// 其他耗时计算(不需要同步)
heavyCompute();
}
优化后:
java复制public void process() {
readFile();
synchronized(this) {
updateCount();
}
heavyCompute();
}
通过缩小同步块范围,可以显著提升并发性能。根据Amdahl定律,同步区域越小,系统的可扩展性越好。
5.2 避免锁嵌套
锁嵌套容易导致死锁:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) {
// ...
}
}
// 线程2
synchronized(lockB) {
synchronized(lockA) {
// ...
}
}
解决方案:
- 按固定顺序获取锁(如先lockA后lockB)
- 使用tryLock()超时机制
- 通过代码审查确保锁获取顺序一致
5.3 锁对象选择原则
- 对于实例同步,建议使用private final的专用锁对象:
java复制private final Object lock = new Object();
- 避免使用String常量或基础类型包装类作为锁,因为它们可能被JVM缓存重用
- 静态同步锁应该谨慎使用,因为类锁的范围太大
6. 常见问题排查
6.1 死锁检测与解决
死锁的四个必要条件:
- 互斥条件
- 占有且等待
- 不可抢占
- 循环等待
通过jstack可以检测死锁:
bash复制jstack <pid> | grep -A 10 deadlock
示例输出会显示哪些线程在等待哪些锁,形成循环依赖链。
6.2 锁竞争性能分析
使用JVisualVM或Arthas查看线程状态:
- BLOCKED状态线程过多表示锁竞争激烈
- 通过-XX:+PrintSynchronizationStatistics可以输出锁竞争统计
优化方案:
- 减小锁粒度(如ConcurrentHashMap的分段锁思想)
- 使用读写锁(ReadWriteLock)替代独占锁
- 考虑无锁编程(CAS操作)
6.3 内存可见性问题
即使使用synchronized也可能遇到可见性问题:
java复制public class VisibilityIssue {
private boolean flag;
public synchronized void setFlag() {
flag = true;
}
public void checkFlag() { // 没有同步
while (!flag) {
// 可能永远循环
}
}
}
解决方法:
- 所有访问共享变量的方法都加同步
- 将变量声明为volatile
- 使用原子变量(AtomicBoolean等)
7. synchronized与Lock的对比
7.1 功能对比
| 特性 | synchronized | Lock |
|---|---|---|
| 获取超时 | 不支持 | 支持tryLock |
| 可中断 | 不支持 | 支持lockInterruptibly |
| 公平锁 | 非公平 | 可配置公平性 |
| 条件变量 | 单一wait set | 多Condition |
| 代码复杂度 | 简单 | 需要手动释放 |
7.2 性能考量
在JDK 6+版本中,两者的性能差异已经不大。选择建议:
- 简单场景优先用synchronized
- 需要高级功能(如超时、可中断)时用Lock
- 超高并发场景考虑StampedLock
7.3 使用模式对比
synchronized的自动释放:
java复制public void method() {
synchronized(lock) {
// 自动释放
}
}
Lock必须手动释放:
java复制Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// ...
} finally {
lock.unlock(); // 必须手动释放
}
}
在实际项目中,我倾向于在简单的同步场景使用synchronized,因为它的简洁性和可靠性。而在需要更复杂控制流的场景,比如尝试获取锁超时后执行备用逻辑,就会选择Lock接口的实现。
