1. 这不是“理论课”,是我在高并发服务里踩出的血路
“深入浅出多线程系列之五:一些同步构造(上篇)”——看到这个标题,别急着划走。它不是教科书里那种堆砌 volatile 、 synchronized 语法糖的填鸭式讲解,而是我过去八年在电商秒杀系统、金融实时风控引擎、IoT设备管理平台这三类典型高并发场景中,亲手用 ReentrantLock 、 CountDownLatch 、 CyclicBarrier 、 Semaphore 反复试错、压测、回滚、再上线后沉淀下来的实操手册。标题里那个“上篇”很关键:我们今天只聊 显式锁与协调型同步器 ,不碰 Future 、 CompletableFuture 这些异步编排工具,更不提前进入 Phaser 或 StampedLock 这种进阶选手——那些留到下篇,因为90%的线上事故,根源就藏在今天要讲的这四个构造里。
我见过太多人把 CountDownLatch 当“等所有线程结束”的万能胶水,结果在订单超时关单服务里,因一个子任务卡死导致整个关单流程挂起30分钟;也见过团队为提升吞吐量盲目把 synchronized 块替换成 ReentrantLock ,却忘了配置公平性参数,在促销高峰时引发线程饥饿,订单创建成功率直接掉到62%。这些都不是概念错误,而是对同步构造 行为边界、资源语义、失败传播机制 缺乏肌肉记忆级的理解。所以这篇内容的核心价值很直白:帮你建立一套“看到代码就能预判线程行为”的直觉——比如读到 new Semaphore(5) ,你脑子里立刻浮现出“最多5个线程能同时穿过这个门禁”,而不是先去翻JDK文档;看到 latch.await(10, TimeUnit.SECONDS) ,你条件反射想到“这里必须有且仅有count个线程调用countDown(),否则10秒后所有等待线程会带着TimeoutException醒来,且latch状态永久失效”。
适合谁?如果你正在写需要处理并行任务的业务代码(比如批量导入用户数据、聚合多个API结果、分片计算报表),或者负责维护一个QPS过万的服务,又或者刚被面试官问倒“ ReentrantLock 比 synchronized 多了什么”,那这篇就是为你写的。不需要你背源码,但要求你愿意跟着我一起,在脑中模拟线程调度的每一步——毕竟,多线程的真相从来不在文档里,而在CPU调度器切换线程的那几纳秒间隙中。
2. 同步构造的本质:不是“加锁”,而是“协商资源使用权”
2.1 所有同步构造都服务于一个底层契约
很多人一提同步就想到“锁住资源”,这是巨大误区。真正决定程序正确性的,从来不是“谁占着资源”,而是“谁有资格申请资源”。 ReentrantLock 、 CountDownLatch 、 CyclicBarrier 、 Semaphore 这四个构造,表面形态差异极大,但内核共享同一套设计哲学: 它们都是线程间达成资源使用协议的协商工具,而非资源本身的看门人 。
举个生活化例子:想象一栋写字楼的会议室预订系统。 ReentrantLock 像一把实体钥匙——只有拿到钥匙的人才能进会议室,用完必须归还; Semaphore 则像会议室门禁卡的发放数量限制——系统总共发5张卡,同时最多5人能进门,但没人管你进去后干啥; CountDownLatch 好比会议签到台——主持人设了10个签到点,必须等10个人全部签完,会议才开始,签到点本身不提供座位; CyclicBarrier 则是电梯轿厢——所有人必须等齐8个人才关门上升,少一个都不动,而且电梯到了楼层还能重复使用。
这个比喻的关键在于: 所有工具都不存储业务数据,也不执行业务逻辑,它们只负责在特定条件下阻塞或唤醒线程 。当你在代码里写 lock.lock() ,你不是在“锁住某个变量”,而是在向JVM申请一个许可;当 latch.await() 返回,你得到的不是数据,而是一个“现在可以安全执行后续逻辑”的信号。这种认知转变至关重要——它让你从“怎么防止数据被改乱”的被动防御,转向“如何让线程按预期节奏协作”的主动设计。
提示:JDK中所有同步构造的实现都基于
AbstractQueuedSynchronizer(AQS)框架。AQS本质是个双向链表队列+一个int类型的state变量。state代表资源的抽象状态(如锁的持有次数、信号量的剩余许可数、倒计时的剩余数值),而队列则管理所有因申请资源失败而被挂起的线程。理解这点,你就明白为什么ReentrantLock能支持可重入、公平/非公平模式,而Semaphore能精确控制并发数——它们只是对state和队列操作策略的不同封装。
2.2 为什么必须区分“独占”与“共享”语义
AQS将同步状态分为两种模式: EXCLUSIVE (独占)和 SHARED (共享)。这个区分直接决定了构造器的行为天花板:
-
ReentrantLock是典型的EXCLUSIVE模式:同一时刻,只有一个线程能成功获取state(即获得锁)。它的state值代表重入次数,每次lock()加1,unlock()减1,为0时释放所有权。 -
Semaphore、CountDownLatch、CyclicBarrier则属于SHARED模式:多个线程可同时获取state,只要state值允许。Semaphore的state是剩余许可数,CountDownLatch的state是剩余倒计数值,CyclicBarrier内部用ReentrantLock+Condition组合实现,其“等待线程数”由另一个state变量维护。
这个区别带来一个致命实践陷阱: 永远不要试图用 ReentrantLock 实现类似 Semaphore 的限流功能 。我曾见过有同事为控制数据库连接池使用量,写这样的代码:
// ❌ 危险示范:用ReentrantLock模拟限流
private final ReentrantLock lock = new ReentrantLock();
private int permits = 5;
public void acquire() throws InterruptedException {
while (permits <= 0) {
Thread.sleep(10); // 错误!忙等待消耗CPU
}
lock.lock(); // 实际只锁住一个线程,无法控制并发数
permits--;
}
这段代码问题重重: while 循环是忙等待, lock.lock() 只保证 permits-- 原子性,但完全无法阻止第6个线程在第1个线程 permits-- 前就进入循环。正确解法是直接用 Semaphore :
// ✅ 正确:用原生Semaphore
private final Semaphore semaphore = new Semaphore(5);
public void acquire() throws InterruptedException {
semaphore.acquire(); // 阻塞直到获得许可
}
Semaphore 的 acquire() 方法会原子性地将 state 减1,若减后为负,则将当前线程加入AQS队列挂起; release() 则原子性加1,并唤醒队列头节点。这种基于AQS的原子操作,才是高并发下可靠的资源协调基础。
2.3 “可重入”不是特性,而是避免死锁的生存法则
ReentrantLock 的“可重入”常被当作高级功能宣传,但在真实系统中,它是防止死锁的刚需。考虑一个电商库存扣减服务:
public class InventoryService {
private final ReentrantLock lock = new ReentrantLock();
public void deductStock(Long skuId, int quantity) {
lock.lock();
try {
// 检查库存
if (getStock(skuId) < quantity) {
throw new InsufficientStockException();
}
// 扣减库存
updateStock(skuId, -quantity);
// 发送扣减成功事件(可能触发其他服务调用)
eventPublisher.publish(new StockDeductedEvent(skuId, quantity));
} finally {
lock.unlock();
}
}
// 事件处理器中可能再次调用本类方法
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
// ... 处理逻辑

791

被折叠的 条评论
为什么被折叠?



