ReentrantLock 线程安全揭秘:从“锁”到“重入”的魔法 🔐🧵
先说结论:是的,ReentrantLock 是线程安全的,而且它在 JDK 中是“自包含线程安全”的典范。但别急着关页面,它的“魔法”比你想象的精妙得多!🚀
一、线程安全 ≠ 天生安全,而是“设计出来的安全” 🏗️🔒
很多人误以为“锁”本身就是线程安全的。错!锁的“线程安全”是指它自身的内部状态在被多个线程并发操作时,不会发生数据竞争和状态不一致。 简单说:多个线程同时调用lock()、unlock(),锁不会自己“发疯”。
为什么这很重要?想象一下:
// 如果锁自己都不安全...
Thread A: lock.lock(); // 成功获取锁
Thread B: lock.lock(); // 也“成功”获取了?天呐!
// 两个线程同时进入临界区,灾难!
ReentrantLock 必须保证:在任何时刻,最多只有一个线程能真正“持有”锁。 这是它作为锁的“本分”。
二、ReentrantLock 的线程安全“三板斧” ⚔️
第一板斧:CAS 操作为核心的“原子抢锁” ⚛️
看看ReentrantLock.NonfairSync的lock()方法(简化版):
final void lock() {
if (compareAndSetState(0, 1)) // 关键:CAS 操作!
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
魔法在这里:compareAndSetState(0, 1)是一个 CAS(Compare-And-Swap)原子操作:
- 比较当前
state是否为0(表示锁空闲) - 如果是0,原子性地设置为1(表示锁被占用)
- 整个比较+设置是原子的,不会被线程切换打断
CAS 的底层是 CPU 指令(如 x86 的cmpxchg),硬件保证原子性。这是 ReentrantLock 线程安全的第一道防线。
第二板斧:AQS 队列的“排队管理” 📊
当 CAS 抢锁失败(锁已被占用),线程不会傻等,而是进入 AQS(AbstractQueuedSynchronizer)队列:
// AbstractQueuedSynchronizer.acquire() 简化逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 再次尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入队列并等待
selfInterrupt();
}
AQS 如何保证线程安全?
-
队列节点操作也是 CAS 的:
// 添加节点到队尾 private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // 队列为空 if (compareAndSetHead(new Node())) // CAS设置头节点 tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { // CAS设置尾节点 t.next = node; return t; } } } } -
状态变量的 volatile 保证可见性:
// AQS 的核心状态 private volatile int state; // volatile! private transient volatile Node head; // volatile! private transient volatile Node tail; // volatile!volatile保证了:- 一个线程修改了
state,其他线程立即可见 - 防止指令重排序带来的诡异问题
- 一个线程修改了
第三板斧:可重入的“计数机制” 🔢
可重入是 ReentrantLock 的特色,也是线程安全的难点:
// ReentrantLock.Sync.tryAcquire() 简化
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取当前锁状态
if (c == 0) { // 锁空闲
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 关键判断!
int nextc = c + acquires; // 重入:状态+1
if (nextc < 0) // 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新状态
return true;
}
return false;
}
这里的安全保障:
- 检查当前持有者:
current == getExclusiveOwnerThread()判断是否是当前线程持有的锁 - 状态递增:
state记录重入次数,解锁时递减 - 解锁匹配:必须解锁相同次数才能真正释放锁
三、公平锁 vs 非公平锁:不同的策略,同样的安全 🎭
非公平锁(默认):"插队"也是安全的
// NonfairSync.lock() - 允许插队
final void lock() {
if (compareAndSetState(0, 1)) // 先尝试直接获取,不管队列
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
安全保证:即使允许插队,插队操作也是CAS原子的,不会让两个线程同时插队成功。
公平锁:"排队"的安全
// FairSync.tryAcquire() 简化
protected final boolean tryAcquire(int acquires) {
// ... 检查是否有前驱节点
if (!hasQueuedPredecessors()) // 关键:前面没人排队才获取
return super.tryAcquire(acquires);
return false;
}
安全保证:检查队列状态和获取锁的操作是原子的,不会出现"检查时没人,获取时突然有人"的竞态条件。
四、从源码看“释放锁”的安全性 🔓
解锁同样需要线程安全:
// ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
// AQS.release()
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 减少重入计数
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 不是持有者不能释放!
boolean free = false;
if (c == 0) { // 完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 更新状态
return free;
}
安全要点:
- 状态递减是原子的:虽然
setState不是原子,但只有锁持有者才能调用 - 持有者验证:防止非法释放
- 唤醒队列操作:
unparkSuccessor也考虑了并发情况
五、工作中的“防坑”指南 🕳️
坑1:认为“有锁就线程安全”
事实:锁只保护临界区,不保护你的业务逻辑。比如:
private List<String> list = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
public void addIfAbsent(String item) {
lock.lock();
try {
if (!list.contains(item)) { // 这里安全
list.add(item); // 这里也安全
}
} finally {
lock.unlock();
}
// 但list本身不是线程安全的!
// 其他地方如果直接操作list,仍然不安全
}
坑2:忘记finally中解锁
lock.lock();
try {
// 业务代码
throw new RuntimeException("Oops!"); // 异常!
// lock.unlock(); // 永远执行不到!
} finally {
lock.unlock(); // 必须放在finally!
}
坑3:误用Condition
Condition condition = lock.newCondition();
// 必须在lock保护下使用!
lock.lock();
try {
condition.await(); // 正确
} finally {
lock.unlock();
}
// 错误!没有获取锁就await
condition.await(); // 抛出IllegalMonitorStateException
坑4:锁泄露(Lock Leak)
public void riskyMethod() {
lock.lock();
if (someCondition) {
return; // 直接返回,忘记解锁!
}
// ... 其他代码
lock.unlock(); // 某些路径执行不到这里
}
六、ReentrantLock vs synchronized:安全对比 ⚔️🆚⚔️
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现机制 | Java代码 + CAS + AQS | JVM内置指令(monitorenter/monitorexit) |
| 线程安全保证 | CAS原子操作 + volatile | JVM内存模型保证 |
| 灵活性 | 可中断、可尝试、可公平 | 简单但固定 |
| 性能 | 高竞争下表现更好 | Java 6+优化后接近 |
| 调试 | 有getQueueLength()等方法 | 工具支持少 |
重要:两者都提供线程安全,但实现方式不同。synchronized 的线程安全由 JVM 保证,ReentrantLock 的线程安全由 JDK 代码 + CAS 保证。
七、如何验证“线程安全”? 🧪
你可以写个测试:
public class ReentrantLockThreadSafeTest {
private final ReentrantLock lock = new ReentrantLock();
private int counter = 0;
public void test() throws InterruptedException {
int threadCount = 100;
CountDownLatch latch = new CountDownLatch(threadCount);
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
lock.lock();
try {
counter++; // 非原子操作,但被锁保护
} finally {
lock.unlock();
}
}
latch.countDown();
};
// 启动100个线程
for (int i = 0; i < threadCount; i++) {
new Thread(task).start();
}
latch.await(); // 等待所有线程完成
System.out.println("Expected: " + (threadCount * 1000) +
", Actual: " + counter); // 应该总是 100000
}
}
如果 ReentrantLock 自己不是线程安全的,这个测试早就失败了!
八、总结:ReentrantLock 线程安全的核心秘诀 🎯
- CAS 原子操作:抢锁、入队、出队都依赖 CAS,硬件保证原子性
- volatile 状态:关键状态变量用 volatile,保证可见性和有序性
- AQS 队列管理:用队列化无序竞争为有序等待
- 正确的锁持有者验证:确保只有持有者能重入和释放
- 异常安全的 unlock:必须放在 finally 块
记住:ReentrantLock 的线程安全是精心设计的结果,不是魔法。它的每一行代码都在与“并发恶魔”搏斗,最终为你呈现出一个简单易用的lock()/unlock()接口。
下次你使用 ReentrantLock 时,可以自信地说: “这个锁,比我的银行账户还安全!” 😄💰
但请记住:锁只是工具,正确使用才是线程安全的最后一道防线。工具再安全,用错了地方,也挡不住 Bug 的侵袭!🐛🛡️