ReentrantLock 线程安全揭秘:从“锁”到“重入”的魔法

ReentrantLock 线程安全揭秘:从"锁"到"重入"的魔法 🔐🧵

先说结论:是的,ReentrantLock 是线程安全的,而且它在 JDK 中是"自包含线程安全"的典范。但别急着关页面,它的"魔法"比你想象的精妙得多!🚀


一、线程安全 ≠ 天生安全,而是"设计出来的安全" 🏗️🔒

很多人误以为"锁"本身就是线程安全的。错!锁的"线程安全"是指它自身的内部状态在被多个线程并发操作时,不会发生数据竞争和状态不一致。 ​ 简单说:多个线程同时调用lock()unlock(),锁不会自己"发疯"。

为什么这很重要?想象一下:

csharp 复制代码
// 如果锁自己都不安全...
Thread A: lock.lock();  // 成功获取锁
Thread B: lock.lock();  // 也"成功"获取了?天呐!
// 两个线程同时进入临界区,灾难!

ReentrantLock 必须保证:在任何时刻,最多只有一个线程能真正"持有"锁。 ​ 这是它作为锁的"本分"。


二、ReentrantLock 的线程安全"三板斧" ⚔️

第一板斧:CAS 操作为核心的"原子抢锁" ⚛️

看看ReentrantLock.NonfairSynclock()方法(简化版):

scss 复制代码
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)队列

scss 复制代码
// AbstractQueuedSynchronizer.acquire() 简化逻辑
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // 再次尝试获取
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 加入队列并等待
        selfInterrupt();
}

AQS 如何保证线程安全?

  1. 队列节点操作也是 CAS 的

    ini 复制代码
    // 添加节点到队尾
    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;
                }
            }
        }
    }
  2. 状态变量的 volatile 保证可见性

    java 复制代码
    // AQS 的核心状态
    private volatile int state;  // volatile!
    private transient volatile Node head;  // volatile!
    private transient volatile Node tail;  // volatile!

    volatile保证了:

    • 一个线程修改了state,其他线程立即可见
    • 防止指令重排序带来的诡异问题

第三板斧:可重入的"计数机制" 🔢

可重入是 ReentrantLock 的特色,也是线程安全的难点:

java 复制代码
// 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;
}

这里的安全保障

  1. 检查当前持有者current == getExclusiveOwnerThread()判断是否是当前线程持有的锁
  2. 状态递增state记录重入次数,解锁时递减
  3. 解锁匹配:必须解锁相同次数才能真正释放锁

三、公平锁 vs 非公平锁:不同的策略,同样的安全 🎭

非公平锁(默认):"插队"也是安全的

scss 复制代码
// NonfairSync.lock() - 允许插队
final void lock() {
    if (compareAndSetState(0, 1))  // 先尝试直接获取,不管队列
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

安全保证 :即使允许插队,插队操作也是CAS原子的,不会让两个线程同时插队成功。

公平锁:"排队"的安全

java 复制代码
// FairSync.tryAcquire() 简化
protected final boolean tryAcquire(int acquires) {
    // ... 检查是否有前驱节点
    if (!hasQueuedPredecessors())  // 关键:前面没人排队才获取
        return super.tryAcquire(acquires);
    return false;
}

安全保证:检查队列状态和获取锁的操作是原子的,不会出现"检查时没人,获取时突然有人"的竞态条件。


四、从源码看"释放锁"的安全性 🔓

解锁同样需要线程安全:

java 复制代码
// 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;
}

安全要点

  1. 状态递减是原子的 :虽然setState不是原子,但只有锁持有者才能调用
  2. 持有者验证:防止非法释放
  3. 唤醒队列操作unparkSuccessor也考虑了并发情况

五、工作中的"防坑"指南 🕳️

坑1:认为"有锁就线程安全"

事实:锁只保护临界区,不保护你的业务逻辑。比如:

csharp 复制代码
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中解锁

csharp 复制代码
lock.lock();
try {
    // 业务代码
    throw new RuntimeException("Oops!");  // 异常!
    // lock.unlock();  // 永远执行不到!
} finally {
    lock.unlock();  // 必须放在finally!
}

坑3:误用Condition

csharp 复制代码
Condition condition = lock.newCondition();
// 必须在lock保护下使用!
lock.lock();
try {
    condition.await();  // 正确
} finally {
    lock.unlock();
}

// 错误!没有获取锁就await
condition.await();  // 抛出IllegalMonitorStateException

坑4:锁泄露(Lock Leak)

csharp 复制代码
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 保证。


七、如何验证"线程安全"? 🧪

你可以写个测试:

csharp 复制代码
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 线程安全的核心秘诀 🎯

  1. CAS 原子操作:抢锁、入队、出队都依赖 CAS,硬件保证原子性
  2. volatile 状态:关键状态变量用 volatile,保证可见性和有序性
  3. AQS 队列管理:用队列化无序竞争为有序等待
  4. 正确的锁持有者验证:确保只有持有者能重入和释放
  5. 异常安全的 unlock:必须放在 finally 块

记住 :ReentrantLock 的线程安全是精心设计的结果 ,不是魔法。它的每一行代码都在与"并发恶魔"搏斗,最终为你呈现出一个简单易用的lock()/unlock()接口。

下次你使用 ReentrantLock 时,可以自信地说: "这个锁,比我的银行账户还安全!" ​ 😄💰

但请记住:锁只是工具,正确使用才是线程安全的最后一道防线。工具再安全,用错了地方,也挡不住 Bug 的侵袭!🐛🛡️

相关推荐
Leo8992 小时前
go 从零单排之 切片 风云再起
后端
L0CK2 小时前
秒杀异步下单业务逻辑梳理
java
不羁到2 小时前
【全平台适用】OpenClaw 进阶教程:Docker 隔离运行 + 浏览器联网 + 飞书流式输出
后端
凌览2 小时前
尤雨溪新公司官宣!Vite+ 正式开源,前端圈要变天了?
前端·javascript·后端
zuoerjinshu2 小时前
【spring专题】编译spring5.3源码
java·后端·spring
JavaGuide2 小时前
鹅厂面试:SELECT * 一定导致索引失效?常见索引失效场景有哪些?
java·数据库·后端·mysql·大厂面试
QuZero2 小时前
Java `volatile` and Memory Model
java·jvm
me8322 小时前
【Java】解决Maven多模块父POM加载失败+IDEA无法新建Java类问题
java·maven·intellij-idea