1. 为什么synchronized是Java面试的必考题?
在Java技术面试中,synchronized关键字出现的频率几乎和"自我介绍"一样高。作为Java并发编程的基石,它不仅是线程安全的保障手段,更是考察候选人底层理解能力的试金石。我经历过上百场技术面试,发现90%的面试官都会以不同形式考察synchronized,这背后有三个深层原因:
首先,synchronized涉及JVM底层实现,能直接反映候选人对Java运行机制的理解深度。当面试官问你"对象头里存储了什么信息?"时,他实际上在考察你是否了解Java对象在内存中的布局结构。
其次,锁升级过程是并发编程的核心知识点。从偏向锁到重量级锁的转换过程,涉及到操作系统内核态切换、线程状态变更等关键概念,这些内容能全面评估候选人的知识体系完整性。
最后,synchronized的使用场景和优化技巧直接关系到系统性能。在高并发场景下,不当的锁使用会导致吞吐量急剧下降,而优秀的开发者必须掌握如何平衡线程安全与性能的关系。
2. synchronized的底层实现机制
2.1 Java对象头与Monitor
每个Java对象在堆内存中都由对象头、实例数据和对齐填充三部分组成。其中对象头包含两类信息:
- Mark Word:存储对象的哈希码、GC分代年龄、锁状态标志等
- Klass Pointer:指向对象元数据的指针
在32位JVM中,Mark Word的结构如下表所示:
| 锁状态 | 25bit | 4bit | 1bit(偏向锁) | 2bit(锁标志) |
|---|---|---|---|---|
| 无锁 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID+epoch | 对象分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | - | - | 00 |
| 重量级锁 | 指向Monitor的指针 | - | - | 10 |
| GC标记 | - | - | - | 11 |
当线程进入synchronized代码块时,JVM会根据竞争情况选择不同的锁实现方式。Monitor(管程)是重量级锁的核心组件,它包含以下关键字段:
- _owner:指向持有锁的线程
- _EntryList:存储阻塞等待锁的线程
- _WaitSet:存储调用wait()方法的线程
2.2 锁升级的全过程
现代JVM采用逐步升级的锁策略来平衡性能与开销:
偏向锁阶段(无竞争场景) 当第一个线程访问同步块时,通过CAS操作将Mark Word中的线程ID替换为当前线程ID。此时锁进入偏向模式,后续该线程进入同步块时只需检查线程ID是否匹配,无需额外同步操作。
轻量级锁阶段(轻度竞争) 当第二个线程尝试获取锁时,偏向锁升级为轻量级锁。JVM会在当前线程的栈帧中创建锁记录(Lock Record),将对象头的Mark Word复制到锁记录中,然后尝试用CAS将对象头的Mark Word替换为指向锁记录的指针。
重量级锁阶段(激烈竞争) 如果CAS操作失败,说明存在多个线程竞争,此时会升级为重量级锁。JVM会向操作系统申请Monitor,未获取锁的线程会被阻塞并放入_EntryList,这涉及到用户态到内核态的切换,开销最大。
重要提示:在JDK 15中,偏向锁已被标记为废弃(JEP 374),因为现代多核处理器环境下偏向锁带来的收益往往小于维护成本。
3. synchronized的实战应用与优化
3.1 四种同步作用域对比
根据修饰对象的不同,synchronized有四种使用方式:
实例方法同步
public synchronized void method() { // 锁对象是当前实例(this) }静态方法同步
public static synchronized void staticMethod() { // 锁对象是当前类的Class对象 }代码块同步(实例对象)
public void method() { synchronized(this) { // 锁对象是当前实例 } }代码块同步(任意对象)
private final Object lock = new Object(); public void method() { synchronized(lock) { // 锁对象是自定义的lock对象 } }
3.2 锁优化的七个关键技巧
减小锁粒度:将大同步块拆分为多个小同步块,典型例子是ConcurrentHashMap的分段锁设计。
降低锁竞争:使用ThreadLocal或副本变量避免共享资源竞争。
替换锁机制:在适当场景用ReentrantLock代替synchronized,它提供更灵活的锁获取方式。
避免嵌套锁:防止多个线程以不同顺序获取相同的锁集合导致死锁。
锁分离策略:读写锁分离,如ReadWriteLock的实现。
锁粗化:对连续多次加锁-解锁操作合并为单次锁操作。
无锁编程:使用CAS操作(如AtomicInteger)替代锁。
4. 高频面试问题深度解析
4.1 synchronized与ReentrantLock的对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现机制 | JVM层面实现 | JDK层面实现(AQS) |
| 锁获取方式 | 自动获取和释放 | 需要显式调用lock()/unlock() |
| 可中断性 | 不支持 | 支持lockInterruptibly() |
| 公平锁 | 非公平 | 可配置公平/非公平 |
| 条件变量 | 只能有一个等待队列 | 可创建多个Condition |
| 性能 | JDK6后性能接近 | 高竞争下略优 |
| 锁绑定多个条件 | 不支持 | 支持 |
4.2 锁的内存语义与happens-before关系
synchronized建立的内存屏障保证了以下happens-before关系:
- 解锁操作happens-before后续对同一锁的加锁操作
- 进入同步块前会清空工作内存,退出时会将变量刷新回主内存
这种内存可见性保证是synchronized线程安全的核心基础。考虑以下代码:
// 线程A synchronized(lock) { x = 1; // 操作1 } // 线程B synchronized(lock) { System.out.println(x); // 操作2 }根据happens-before原则,操作1的结果对操作2一定可见。
5. 生产环境中的锁问题诊断
5.1 常见锁问题识别
死锁检测使用jstack获取线程转储,查找"deadlock"关键词:
jstack -l <pid> > thread_dump.txt锁竞争分析通过JMC(Java Mission Control)的锁分析功能,可以观察到:
- 锁等待时间
- 持有锁的线程
- 竞争最激烈的锁
性能指标监控
- 锁获取平均时间
- 每秒锁获取次数
- 等待线程数
5.2 锁优化实战案例
某电商平台在秒杀场景下遇到性能瓶颈,原始代码如下:
public class SeckillService { private static int stock = 1000; public synchronized boolean seckill() { if(stock > 0) { stock--; return true; } return false; } }优化方案:
- 使用AtomicInteger替代synchronized
- 引入分段锁减少竞争
- 前置库存校验减少锁持有时间
最终实现:
public class OptimizedSeckillService { private final AtomicInteger stock = new AtomicInteger(1000); private final StampedLock lock = new StampedLock(); public boolean seckill() { // 乐观读 long stamp = lock.tryOptimisticRead(); int current = stock.get(); if(current <= 0) return false; if(!lock.validate(stamp)) { // 升级为悲观锁 stamp = lock.readLock(); try { current = stock.get(); } finally { lock.unlockRead(stamp); } } // CAS更新 return stock.compareAndSet(current, current - 1); } }优化后QPS从200提升到5000+,关键是通过锁降级和CAS操作减少了线程阻塞时间。