阿里一面:ReentrantLock如何保证线程安全?

35 阅读3分钟

文章内容收录到个人网站,方便阅读hardyfish.top/

ReentrantLock 的线程安全,本质由两件事托底:互斥内存可见性

互斥保证同一时刻只有一个线程进入临界区(critical section,受保护的共享资源访问区);

内存可见性保证临界区内对共享变量的写入,在解锁后对随后获得锁的线程可见。

互斥:基于 AQS 的“独占状态”与 CAS 争用

ReentrantLock 的底层是 AQS(AbstractQueuedSynchronizer,同步器框架) 。AQS 用一个 state 整数表示同步状态,对独占锁而言通常含义是“是否被占用/占用次数”。

  • 抢锁:线程通过 CAS(Compare-And-Swap,比较并交换)尝试把state从0改为1(或从当前值递增)。

    • 成功:获得锁,成为 owner(持有者)。
    • 失败:进入 AQS 的 CLH 队列(FIFO 等待队列),阻塞/自旋等待后续唤醒。
  • 锁的排他性:只有 owner 才能继续执行临界区代码;非 owner 必须等待 state 归零并被唤醒后再竞争。

这种设计的关键点在于:对 state 的修改使用原子操作(CAS),从而在高并发下也能可靠地裁决“谁赢得锁”。

可重入:用“持有者 + 计数”避免自我死锁

“可重入(reentrant)”指同一线程在持有锁时再次 lock() 不会被自己阻塞。

ReentrantLock 通过两类信息实现:

  • 持有者线程记录:AQS 维护当前独占持有者(owner thread)。

  • 重入次数计数:state充当计数器。

    • 第一次获取锁:state0 变为 1,owner 设为当前线程。
    • 同一线程再次获取:检测 owner 是自己,直接 state++
    • 释放锁:state--;直到减到 0 才真正释放所有权并唤醒队列中的后继线程。

因此,临界区内调用链较深、方法间层层加锁时,不会发生“自己把自己锁死”的问题。

阻塞与唤醒:队列化等待减少竞争风暴

当抢锁失败,线程不会无休止忙等,而是进入 AQS 队列并在合适时机被唤醒:

  • 入队后,线程可能 park(挂起),释放 CPU。
  • 解锁时,持有者把 state 释放到 0,再按策略唤醒队列中的后继节点竞争。

队列化的价值在于把竞争从“所有线程同时冲刺”变成“排队接力”,显著降低上下文切换与缓存抖动带来的损耗。

内存可见性:lock/unlock 的 happens-before 语义

线程安全不只需要“同一时刻只有一个线程进来”,还需要“前一个线程写过的数据,后一个线程读得到”。

ReentrantLock 提供与 synchronized 类似的内存语义:

  • unlock 之前的写,在 随后某线程成功 lock 之后的读 中可见(happens-before)。
  • 这依赖于 AQS 内部对 state 等字段的 volatile 语义 与 CAS/park-unpark 等操作在 Java 内存模型下形成的屏障效果。

直观理解:解锁像“刷盘提交”,加锁像“重新加载最新版本”,保证共享变量不会读到旧值。

公平与非公平:安全性相同,调度策略不同

ReentrantLock 有两种策略:

  • 非公平锁(默认) :新来的线程可能“插队”直接 CAS 抢到锁,吞吐通常更高。
  • 公平锁:倾向于按队列先来后到获取锁,减少饥饿(starvation)风险。

两者都满足互斥与可见性要求,线程安全语义一致,差别在于获得锁的次序与性能特征。

关键用法约束:释放必须与获取配对

ReentrantLock 的线程安全依赖“锁的协议”被正确遵守:

  • 释放次数必须与获取次数匹配(重入多少次就要 unlock 多少次),否则会造成锁长期不释放。
  • 通常在 finally 中释放,避免异常路径遗留锁导致其他线程永久阻塞。

这套机制合起来,使 ReentrantLock 在并发访问共享资源时,既能保证互斥,又能保证内存可见性,从而实现可验证的线程安全。