本文结合社交平台高并发抢票与工业物联网设备监控两个真实项目,聊聊这两把锁到底该怎么选。

一、问题的起点

在做兴趣社交平台"趣玩搭"的高并发抢票模块时,热门活动上线瞬间会涌入数千并发请求争抢有限名额;而在此前的凯泵智联项目中,设备状态数据会被监控采集线程、诊断分析线程、大屏展示线程同时读写。两个场景都涉及并发安全,但竞争程度和业务语义截然不同。选错锁机制,轻则性能受损,重则线程池耗尽、服务不可用。

这让我不得不认真梳理 synchronizedReentrantLock 的区别,形成自己的选型方法论。

二、核心区别:不只是"一个是关键字,一个是类"

2.1 实现层面

synchronized 是 JVM 内置的关键字,由字节码指令 monitorenter/monitorexit 实现,锁的获取和释放完全由 JVM 自动管理。代码简洁,但也意味着你对加锁过程几乎没有控制权。

ReentrantLockjava.util.concurrent.locks 包下的 API 级别锁,底层基于 AQS(AbstractQueuedSynchronizer)框架实现。需要手动调用 lock() 获取锁、在 finally 块中调用 unlock() 释放锁。代码多了几行,但换来了更精细的控制能力。

2.2 功能差异:ReentrantLock 多出的三把"钥匙"

可中断获取锁。 lockInterruptibly() 允许一个正在等待锁的线程被中断唤醒,从而避免死等。synchronized 一旦进入等待,只能等到获取锁为止,无法响应中断。

超时获取锁。 tryLock(long timeout, TimeUnit unit) 可以设置一个等待时限,超时则放弃并返回 false。这在高并发场景下极其重要——与其让几千个线程死等一把锁,不如让它们快速失败。

公平锁选项。 new ReentrantLock(true) 可以创建公平锁,按照线程等待的先后顺序分配锁。synchronized 只有非公平模式,无法保证等待顺序。

此外,ReentrantLock 可以绑定多个 Condition 对象,实现分组唤醒(比如生产者只唤醒消费者,而不是唤醒所有等待线程),而 synchronizedwait/notify 只有一个等待队列,notifyAll() 会唤醒所有线程造成不必要的竞争。

2.3 性能对比

JDK 6 之后,JVM 对 synchronized 做了大量优化:偏向锁(单线程反复获取同一把锁时零开销)、轻量级锁(CAS 替代重量级的 monitor)、锁粗化(合并相邻的同步块)、锁消除(JIT 发现锁对象不会逃逸时直接去掉锁)。

在低竞争场景下,两者性能基本持平。在高竞争场景下,ReentrantLocktryLock 快速失败能力可以避免大量线程阻塞,整体吞吐量更优。

三、实战选型:两个真实场景

3.1 低竞争场景:设备状态缓存(用 synchronized)

在凯泵智联项目中,设备状态数据的本地缓存需要线程安全保护。读多写少,竞争程度很低,代码块也非常短——只是一个简单的 Map.put() 操作。

public class DeviceStatusCache {
    private final Map<String, DeviceStatus> localCache = new HashMap<>();
​
    // 竞争低、代码块短,synchronized 简洁可靠
    public synchronized void updateDeviceStatus(String deviceId, DeviceStatus status) {
        localCache.put(deviceId, status);
    }
​
    public synchronized DeviceStatus getDeviceStatus(String deviceId) {
        return localCache.get(deviceId);
    }
}

选择理由: 代码简洁,不需要操心忘记释放锁的问题,JVM 的偏向锁优化在低竞争下几乎零额外开销。在这个场景里,用 ReentrantLock 纯属过度设计。

3.2 高竞争场景:抢票前置令牌校验(用 ReentrantLock)

在趣玩搭的抢票流程中,进入 Redis Lua 脚本扣减库存之前,有一层本地的令牌校验逻辑(防止无效请求打到 Redis)。抢票开始的瞬间,几千个请求同时到达,竞争极其激烈。

public class TicketGrabService {
    private final ReentrantLock localLock = new ReentrantLock();
​
    public TicketResult grabTicket(Long activityId, Long userId) throws InterruptedException {
        // 最多等200ms,等不到就快速失败
        if (localLock.tryLock(200, TimeUnit.MILLISECONDS)) {
            try {
                // 本地令牌校验通过后,走 Redis Lua 扣减库存
                return redisTicketService.deductStock(activityId, userId);
            } finally {
                localLock.unlock(); // 必须在 finally 中释放
            }
        } else {
            // 快速失败,友好提示
            throw new BusinessException("活动太火爆,请稍后重试");
        }
    }
}

选择理由: 如果用 synchronized,几千个线程会全部阻塞在锁上,Tomcat 线程池很快被耗尽,其他正常接口也会受影响,造成整个服务不可用。而 tryLock(200ms) 让等不到锁的线程快速返回,接口平均响应时间控制在 200ms 以内,用户得到"请重试"的提示远好过等十几秒后超时。

四、选型决策树

经过多个项目的实践,我总结了一个简单的选型决策流程:

  1. 需要可中断、超时、公平锁、多 Condition 中的任何一个吗? → 是 → ReentrantLock

  2. 并发竞争是否激烈? → 是(高并发热点路径) → 优先考虑 ReentrantLock(利用 tryLock 快速失败)

  3. 以上都不需要,代码块短且简单?synchronized(简洁、不易出错、JVM 自动优化)

一个额外的提醒:ReentrantLockunlock() 必须放在 finally 块中。我在做代码 Review 时不止一次看到忘记释放锁导致死锁的情况。如果团队经验参差不齐,低竞争场景下用 synchronized 反而更安全。

五、总结

不存在"哪个更好"的绝对答案。synchronized 是简洁可靠的默认选择,ReentrantLock 是高竞争和复杂控制场景下的精准武器。理解它们各自的设计意图和适用边界,根据实际的业务场景和并发特征做选择,才是真正的"深入理解并发编程"。

维度

synchronized

ReentrantLock

实现层面

JVM 关键字,自动管理

API 级别,手动加锁释放

可中断

不支持

支持(lockInterruptibly)

超时获取

不支持

支持(tryLock)

公平锁

不支持

支持

多条件等待

单一等待队列

多个 Condition

性能(低竞争)

优(偏向锁/轻量级锁)

性能(高竞争)

一般(线程阻塞堆积)

优(快速失败策略)

代码安全性

高(自动释放)

需 try-finally 保证


如果这篇文章对你有帮助,欢迎访问我的博客 robinzhu.top 获取更多实战分享。


本站由 困困鱼 使用 Stellar 创建。