1. 面试官为何对锁机制穷追不舍?
那是我记忆中最煎熬的15分钟。面试官从最基础的synchronized用法开始,层层递进到分布式锁的实现原理,问题像连珠炮般砸过来。事后复盘才明白,锁机制之所以成为Java面试的"必考题",根本原因在于它直指并发编程的核心痛点。
在真实业务场景中,我曾遇到过这样的案例:某电商促销活动时,库存扣减出现超卖。日志显示多个线程同时判断库存充足后执行扣减,最终导致库存变为负数。这就是典型的竞态条件问题——当多个线程访问共享资源(这里是库存数量)且至少有一个线程执行写操作时,如果没有适当的同步机制,程序行为将变得不可预测。
关键认知:锁的本质是控制多个线程对共享资源的访问顺序,解决的是可见性和有序性问题,而不仅仅是互斥。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从synchronized看Java锁的演进
2.1 最原始的同步方案
synchronized关键字是大多数Java开发者接触的第一个锁机制。它的基础用法非常简单:
java复制public class Counter {
private int count;
// 同步方法
public synchronized void increment() {
count++;
}
// 同步代码块
public void decrement() {
synchronized(this) {
count--;
}
}
}
但在实际项目中,这种粗粒度的锁会带来严重的性能问题。某次性能调优中,我发现一个使用类级别synchronized的订单处理服务,TPS始终无法突破200。通过JProfiler分析,大量线程阻塞在锁获取阶段。
2.2 锁优化的技术演进
JDK1.6后,synchronized实现了从重量级锁到轻量级锁的升级路径:
- 偏向锁:单线程访问时,直接在对象头记录线程ID
- 轻量级锁:少量线程竞争时,通过CAS自旋尝试获取锁
- 重量级锁:高竞争时,升级为操作系统级别的互斥量
我曾用JMH做过对比测试:在低竞争场景下,经过优化的synchronized性能与ReentrantLock基本持平。但在高并发场景(>1000线程)中,ReentrantLock的吞吐量要高出30%左右。
3. 现代Java锁的实战选择
3.1 显式锁ReentrantLock
与synchronized相比,ReentrantLock提供了更灵活的控制:
java复制Lock lock = new ReentrantLock();
try {
lock.lock();
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放
}
其核心优势包括:
- 可中断的锁获取(lockInterruptibly)
- 尝试非阻塞获取锁(tryLock)
- 公平锁实现(new ReentrantLock(true))
在消息队列的消费者实现中,我使用tryLock实现了优雅降级:当无法立即获取锁时,转而将任务暂存到本地队列,避免线程阻塞。
3.2 读写锁的妙用
对于读多写少的场景,ReentrantReadWriteLock能大幅提升性能:
java复制ReadWriteLock rwLock = new ReentrantReadWriteLock();
void readData() {
rwLock.readLock().lock();
try {
// 并发读操作
} finally {
rwLock.readLock().unlock();
}
}
某配置中心服务改造中,引入读写锁后,配置读取的吞吐量提升了8倍。但要注意写锁的饥饿问题——持续不断的读请求可能导致写线程长时间等待。
4. 分布式环境下的锁挑战
4.1 Redis分布式锁的陷阱
基于Redis的SETNX命令实现分布式锁是常见方案:
java复制String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
try {
// 获取锁成功
} finally {
// Lua脚本保证原子性释放
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
}
}
但在实际生产中遇到过两个致命问题:
- 时钟漂移导致锁提前释放(服务器时间不同步)
- 主从切换时的锁失效(异步复制延迟)
4.2 更可靠的分布式锁实现
对于关键业务,建议采用:
- RedLock算法(多Redis实例投票)
- Zookeeper临时顺序节点
- 数据库唯一约束(配合心跳检测)
在支付系统中,我们最终选择了基于Zookeeper的方案,虽然性能稍差(约500TPS),但保证了强一致性。关键实现点是利用临时节点的会话特性,客户端断开连接时自动释放锁。
5. 锁使用中的血泪教训
5.1 死锁的四种经典场景
- 交叉依赖锁:
java复制// 线程1
synchronized(A) {
synchronized(B) { ... }
}
// 线程2
synchronized(B) {
synchronized(A) { ... }
}
- 锁内调用外部方法(可能获取其他锁)
- 未释放锁的异常路径
- 自旋锁饥饿(长时间占用CPU)
某次线上故障排查中,使用jstack发现所有线程都在等待一个第三方jar包中的锁。最终定位是该jar包在静态初始化块中获取了锁,但初始化失败后未释放。
5.2 性能优化实践
- 锁粒度控制:从类锁细化到对象锁
- 锁分段:ConcurrentHashMap的分段锁思想
- 无锁编程:Atomic变量/CAS操作
- 线程本地存储:ThreadLocal避免共享
在交易引擎开发中,我们将全局的订单锁改为按订单ID哈希的分段锁,QPS从1500提升到12000+。关键代码片段:
java复制Striped<Lock> lockStripes = Striped.lock(32);
Lock lock = lockStripes.get(orderId);
lock.lock();
try {
// 处理订单
} finally {
lock.unlock();
}
6. 从JVM角度看锁实现
通过查看对象头信息(64位JVM):
code复制|------------------------------------------------------------------|
| Mark Word (64 bits) | State |
|--------------------------------------|---------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
| ptr_to_lock_record:62 | lock:2 | Lightweight |
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight |
| | lock:2 | Marked for GC |
这个内存布局解释了为什么调用hashCode()会导致偏向锁撤销。实际遇到过因频繁调用System.identityHashCode()导致锁无法偏向的案例。
7. 锁监控与问题诊断
7.1 常用诊断工具
- jstack:查看线程锁持有情况
- JMC(Java Mission Control):可视化锁竞争分析
- Arthas:watch命令监控锁方法调用
某次性能诊断中,通过JMC发现一个"无害"的日志锁(Logger锁)竟成为系统瓶颈,占用了30%的等待时间。最终解决方案是改用异步日志。
7.2 锁统计技巧
自定义锁监控组件示例:
java复制public class MonitoredReentrantLock extends ReentrantLock {
private long totalWaitTime;
@Override
public void lock() {
long start = System.nanoTime();
super.lock();
totalWaitTime += System.nanoTime() - start;
}
public long getTotalWaitTime() {
return TimeUnit.NANOSECONDS.toMillis(totalWaitTime);
}
}
这个简单的扩展帮助我们发现了一个数据库连接池的锁竞争问题——95%的线程时间花在等待获取连接上。
8. 锁与内存模型的深层关系
很多人忽略了Java内存模型(JMM)与锁的关系。实际上,锁的释放会建立happens-before关系,保证内存可见性。这个特性在双重检查锁定(DCL)模式中至关重要:
java复制class Singleton {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Instance();
}
}
}
return instance;
}
}
没有volatile修饰时,由于指令重排序,其他线程可能看到未初始化完成的对象。这个问题在ARM架构的服务器上更容易出现,因为其内存模型更宽松。
9. 锁的未来发展趋势
随着硬件发展,锁的实现也在不断进化:
- 硬件事务内存(Intel TSX):CPU级乐观锁
- 协程友好锁:与虚拟线程(Project Loom)配合
- 无等待算法:如RCU(Read-Copy-Update)
在GraalVM的早期测试中,我们发现其对于锁消除(Lock Elision)的优化比HotSpot更激进,某些场景下甚至能自动将悲观锁转为乐观锁。
