1. 面试官为什么关心锁的区别?
这个问题几乎出现在90%的中高级Java开发岗位面试中。面试官抛出这个问题时,通常有三个考察维度:
- 基础扎实度:你是否真正理解Java并发编程的底层机制
- 实战经验:你是否在实际项目中遇到过并发问题并合理选择解决方案
- 技术演进认知:你是否了解不同锁机制的适用场景和演进关系
我在美团和蚂蚁的面试中,都曾被要求在白板上画出synchronized的锁升级过程,并对比AQS的实现原理。这直接决定了面试官对你技术深度的判断。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. synchronized的底层实现剖析
2.1 对象头与Mark Word
每个Java对象头部都包含Mark Word字段,32位JVM中占4字节,64位JVM中占8字节。这个字段在不同锁状态下会存储不同内容:
code复制|---------------------------------------------------------------------|
| 锁状态 | 25bit | 4bit | 1bit(偏向锁) | 2bit(锁标志) |
|--------------|----------------|--------------|-------------|-------------|
| 无锁 | hashCode | 分代年龄 | 0 | 01 |
| 偏向锁 | ThreadID+epoch | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录 | - | - | 00 |
| 重量级锁 | 指向互斥量指针 | - | - | 10 |
| GC标记 | - | - | - | 11 |
|---------------------------------------------------------------------|
关键点:锁升级过程是不可逆的,这是很多开发者容易误解的地方
2.2 锁升级全流程
-
偏向锁阶段(Biased Locking)
- 首次获取锁时,CAS操作将Mark Word中的ThreadID设置为当前线程ID
- 后续同一线程进入同步块只需检查ThreadID是否匹配
- 适用场景:明确知道只会有一个线程访问的场景
-
轻量级锁(Lightweight Lock)
- 当出现锁竞争时,撤销偏向锁升级为轻量级锁
- 线程栈中创建Lock Record,通过CAS将Mark Word复制到Lock Record
- 使用自旋(默认10次)尝试获取锁,避免线程阻塞
-
重量级锁(Heavyweight Lock)
- 自旋超过阈值后升级为重量级锁
- 向操作系统申请互斥量(mutex),线程进入阻塞状态
- 涉及用户态到内核态的切换,性能开销最大
2.3 锁消除与锁粗化
JVM还会做两种重要优化:
- 锁消除:通过逃逸分析发现同步代码不可能被多线程访问时,直接消除锁
- 锁粗化:检测到连续多个同步块使用同一把锁时,合并为一个更大的同步块
3. Lock接口的实现原理
3.1 AQS核心设计
AbstractQueuedSynchronizer(AQS)是Lock的基石,其核心是:
java复制// 共享变量,使用volatile保证可见性
private volatile int state;
// 双向链表实现的等待队列
private transient volatile Node head;
private transient volatile Node tail;
关键方法:
- tryAcquire():尝试获取锁
- addWaiter():将线程加入等待队列
- acquireQueued():队列中的线程自旋获取锁
3.2 公平锁 vs 非公平锁
以ReentrantLock为例:
java复制// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接尝试获取
setExclusiveOwnerThread(current);
return true;
}
}
// ...重入逻辑
}
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() && // 先检查队列
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// ...重入逻辑
}
3.3 Condition实现原理
每个ConditionObject维护一个条件队列:
java复制public class ConditionObject implements Condition {
private transient Node firstWaiter;
private transient Node lastWaiter;
public final void await() throws InterruptedException {
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
// ...
}
}
4. 核心差异对比
4.1 实现层面差异
| 维度 | synchronized | Lock |
|---|---|---|
| 实现机制 | JVM层面通过monitor实现 | Java代码实现(AQS) |
| 锁获取 | 自动获取和释放 | 需要显式调用lock()/unlock() |
| 中断响应 | 不支持 | 支持lockInterruptibly() |
| 公平性 | 非公平 | 可配置公平/非公平 |
| 条件变量 | 只能通过wait/notify | 支持多个Condition |
4.2 性能对比
在低竞争场景下:
- synchronized经过锁升级优化后性能接近Lock
- Lock的CAS操作仍有轻微优势(约5-10%)
在高竞争场景下:
- synchronized会直接升级为重量级锁
- Lock可以通过tryLock实现更灵活的退避策略
4.3 使用场景选择
优先使用synchronized的情况:
- 简单的同步块(如单方法内的同步)
- 不需要高级功能(超时、中断等)
- 资源竞争不激烈的场景
必须使用Lock的情况:
- 需要尝试非阻塞获取锁(tryLock)
- 需要支持中断的锁获取
- 需要公平锁机制
- 需要绑定多个条件变量
5. 生产环境中的锁优化经验
5.1 锁粒度控制
错误示例:
java复制public class UserService {
private final Object lock = new Object();
public void updateUser(User user) {
synchronized(lock) { // 锁范围过大
// 校验逻辑...
// 数据库操作...
// 日志记录...
}
}
}
优化方案:
java复制public class UserService {
private final Map<Long, Object> idLocks = new ConcurrentHashMap<>();
public void updateUser(User user) {
Object lock = idLocks.computeIfAbsent(user.getId(), k -> new Object());
synchronized(lock) { // 细粒度锁
// 核心业务逻辑
}
}
}
5.2 避免死锁的编码规范
- 锁排序法:所有线程按固定顺序获取锁
- 设置锁超时:使用tryLock(timeout)
- 静态代码分析工具检测:
bash复制mvn spotbugs:check # 使用SpotBugs检测死锁风险
5.3 监控锁争用
通过JMX查看锁情况:
java复制ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadMXBean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] infos = threadMXBean.getThreadInfo(threadIds);
for (ThreadInfo info : infos) {
System.out.println(info.getLockName() + "被" + info.getThreadName() + "阻塞");
}
}
6. 高频面试题深度解析
6.1 为什么synchronized不是公平锁?
设计决策基于两点:
- 性能优先:非公平锁减少了线程切换,吞吐量更高
- 历史原因:早期JVM实现简单,公平锁需要更复杂的队列管理
实测数据:在16核机器上,非公平锁的吞吐量比公平锁高30-40%
6.2 synchronized能实现读写分离吗?
不能,但可以通过ReadWriteLock实现:
java复制ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock(); // 共享锁
Lock writeLock = rwLock.writeLock(); // 排他锁
6.3 分布式锁的实现选择
对比方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Redis SETNX | 性能高(10w+ QPS) | 非强一致 |
| Zookeeper | 强一致 | 性能较低(1w QPS) |
| 数据库行锁 | 无需额外组件 | 性能差(1k QPS) |
推荐Redlock算法的改进实现:
java复制RLock lock = redisson.getLock("orderLock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}
7. JUC锁的演进趋势
Java 8之后的改进:
-
StampedLock:乐观读模式提升读多写少场景性能
java复制StampedLock sl = new StampedLock(); long stamp = sl.tryOptimisticRead(); // 不阻塞 if (!sl.validate(stamp)) { // 检查是否被修改 stamp = sl.readLock(); // 退化为悲观读 } -
VarHandle:提供更细粒度的内存访问控制,替代部分锁场景
-
虚拟线程兼容:在Java 19+中,Lock实现已适配虚拟线程
我在实际项目中发现,在超高并发(10w+ QPS)场景下,合理组合使用:
- 读多写少:StampedLock
- 写多:ReentrantLock + Condition
- 简单同步:synchronized
可以取得最佳性能表现。关键是要通过JMX持续监控锁竞争情况,动态调整锁策略。
