1. 为什么我们需要超越 synchronized?
在 Java 并发编程领域,synchronized 关键字就像是一把瑞士军刀 - 简单易用但功能有限。我见过太多开发者把 synchronized 当作解决所有并发问题的银弹,直到他们在高并发场景下遇到性能瓶颈或是复杂的同步需求时才意识到问题的严重性。
synchronized 的局限性主要体现在三个方面:首先,它是非公平锁,无法保证等待时间最长的线程最先获取锁;其次,它不支持中断,一旦线程开始等待锁就无法被中断;最重要的是,它缺乏灵活的锁获取机制,比如尝试获取锁、定时等待等功能。这些限制在构建复杂的并发系统时会成为明显的障碍。
实际案例:在一个电商平台的秒杀系统中,使用 synchronized 导致大量线程阻塞,最终系统吞吐量下降到无法接受的水平。改用 ReentrantLock 后,通过公平锁策略和尝试获取锁机制,系统性能提升了3倍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReentrantLock 的核心优势解析
2.1 可重入性设计原理
ReentrantLock 的可重入特性是其最基础也是最重要的特性。所谓可重入,指的是同一个线程可以多次获取同一把锁而不会导致死锁。这个特性通过一个计数器实现 - 每次获取锁计数器加1,释放锁时计数器减1,只有当计数器归零时锁才真正释放。
java复制public void methodA() {
lock.lock();
try {
methodB(); // 可重入调用
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock(); // 同一线程可再次获取锁
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
2.2 公平锁与非公平锁的抉择
ReentrantLock 提供了公平锁和非公平锁两种模式。公平锁按照线程请求的顺序分配锁,而非公平锁则允许插队。虽然公平锁听起来更合理,但在实际应用中非公平锁的性能通常更好,因为减少了线程切换的开销。
java复制// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();
性能测试数据:在100个线程竞争的场景下,非公平锁的吞吐量比公平锁高出约40%。但在线程等待时间差异较大的场景中,公平锁能提供更可预测的性能。
2.3 灵活的锁获取机制
ReentrantLock 提供了多种锁获取方式,这是它相比 synchronized 的最大优势:
- tryLock():尝试获取锁,立即返回成功或失败
- tryLock(long timeout, TimeUnit unit):带超时的尝试获取锁
- lockInterruptibly():可中断的获取锁方式
java复制if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 获取锁成功,执行业务逻辑
} finally {
lock.unlock();
}
} else {
// 获取锁超时,执行备用逻辑
}
3. 死锁的预防与排查实战
3.1 死锁产生的四大必要条件
根据Coffman条件,死锁产生必须同时满足以下四个条件:
- 互斥条件:资源一次只能由一个线程持有
- 占有并等待:线程持有资源的同时等待其他资源
- 非抢占条件:已分配的资源不能被强制剥夺
- 循环等待条件:存在一个线程的循环等待链
3.2 死锁预防策略
在实际项目中,我总结出以下有效的死锁预防策略:
- 锁顺序法:强制所有线程按照相同的顺序获取锁
- 锁超时法:使用tryLock而不是lock,设置合理的超时时间
- 资源预分配法:一次性获取所有需要的资源
- 开放调用法:不在持有锁的情况下调用外部方法
java复制// 锁顺序法示例
public void transfer(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
first.lock();
try {
second.lock();
try {
// 转账逻辑
} finally {
second.unlock();
}
} finally {
first.unlock();
}
}
3.3 死锁排查工具与技术
当死锁发生时,我们需要有效的工具来定位问题:
- jstack:最基础的死锁检测工具
- JConsole/VisualVM:图形化线程分析工具
- Thread Dump Analyzer (TDA):专业的线程转储分析工具
- 自定义死锁检测:通过ReentrantLock的getOwner和getQueuedThreads方法
bash复制# 使用jstack检测死锁
jstack <pid> | grep -A 10 "deadlock"
排查技巧:在线上环境,可以设置定时任务定期收集线程转储,当系统出现响应迟缓时立即分析最近的转储文件。
4. 高级应用场景与性能优化
4.1 读写锁的应用场景
对于读多写少的场景,ReentrantReadWriteLock 能显著提升性能。它允许多个读线程同时访问,但写线程独占访问。
java复制ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 读操作
public Object readData() {
readLock.lock();
try {
// 读取数据
} finally {
readLock.unlock();
}
}
// 写操作
public void writeData(Object data) {
writeLock.lock();
try {
// 写入数据
} finally {
writeLock.unlock();
}
}
4.2 条件变量的精妙使用
Condition 接口提供了比 Object 的 wait/notify 更灵活的线程通信机制。一个锁可以关联多个条件,每个条件代表一个不同的等待集。
java复制class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
4.3 性能优化实战技巧
- 减小锁粒度:将一个大锁拆分为多个小锁
- 锁分段技术:ConcurrentHashMap的实现原理
- 无锁编程:尝试使用Atomic变量和CAS操作
- 锁消除:JVM在检测到不可能存在竞争时会消除锁
- 锁粗化:将连续的锁/解锁操作合并为一个
java复制// 锁分段技术示例
class StripedMap {
private static final int N_LOCKS = 16;
private final Node[] buckets;
private final Object[] locks;
public StripedMap(int capacity) {
buckets = new Node[capacity];
locks = new Object[N_LOCKS];
for (int i = 0; i < N_LOCKS; i++)
locks[i] = new Object();
}
private final int hash(Object key) {
return Math.abs(key.hashCode() % buckets.length);
}
public Object get(Object key) {
int hash = hash(key);
synchronized (locks[hash % N_LOCKS]) {
for (Node m = buckets[hash]; m != null; m = m.next)
if (m.key.equals(key)) return m.value;
}
return null;
}
public void put(Object key, Object value) {
int hash = hash(key);
synchronized (locks[hash % N_LOCKS]) {
// 插入逻辑
}
}
static class Node {
Object key, value;
Node next;
}
}
5. 常见陷阱与最佳实践
5.1 必须避免的六大错误
- 忘记释放锁:务必在finally块中释放锁
- 锁泄露:持有锁的方法抛出异常导致锁无法释放
- 嵌套锁顺序不一致:这是死锁的最常见原因
- 过度同步:将不需要同步的代码也包含在同步块中
- 活锁:线程不断重试失败的操作
- 锁粒度过大:导致并发度下降
5.2 最佳实践清单
根据多年经验,我总结了以下ReentrantLock使用的最佳实践:
- 总是立即在lock()调用后使用try块
- 在finally块中释放锁
- 不要将锁对象声明为方法内的局部变量
- 考虑使用定时锁而非无限等待
- 优先使用非公平锁,除非有特殊需求
- 为锁和条件变量使用有意义的名称
- 考虑使用更高级的并发工具如Semaphore或CountDownLatch
java复制// 正确的锁使用模式
private final Lock orderLock = new ReentrantLock();
public void processOrder() {
orderLock.lock();
try {
// 临界区代码
} finally {
orderLock.unlock();
}
}
5.3 性能监控与调优
在生产环境中监控锁竞争情况至关重要。我们可以使用以下方法:
- ThreadMXBean:获取锁信息
- ReentrantLock的getQueueLength():监控等待队列长度
- 自定义监控:记录锁等待时间
java复制// 使用ThreadMXBean检测死锁
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] infos = bean.getThreadInfo(threadIds);
for (ThreadInfo info : infos) {
System.out.println("Deadlocked thread: " + info.getThreadName());
}
}
在实际项目中,我发现将锁等待时间超过阈值的记录到日志中,能帮助我们早期发现潜在的并发问题。一个经验值是,如果锁等待时间经常超过100ms,就需要考虑优化锁策略了。
