找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

181

积分

0

好友

21

主题
发表于 昨天 01:58 | 查看: 3| 回复: 0

1 什么是ReentrantLock?:从厕所排队说起

想象一个热门商场的厕所场景:多人同时需要使用,但每次只能容纳一人。这种"独占"访问模式在并发编程中就是典型的互斥访问问题。Java中的ReentrantLock(可重入锁)正是解决这类问题的高效工具,它提供了比传统synchronized关键字更强大、更灵活的锁机制。

官方定义ReentrantLock是Java并发包(java.util.concurrent.locks)中的可重入互斥锁,具备与synchronized相同的并发性和内存语义,同时集成更多高级功能。

简单类比,ReentrantLock如同智能门禁系统,具备三大核心特性:

  • 互斥性:类似厕所门锁,一次仅允许一个线程访问资源
  • 可重入性:同一线程可重复获取同一把锁,避免自我阻塞
  • 灵活性:支持公平性选择、可中断锁获取、超时机制等高级功能

synchronized对比,ReentrantLock如同功能全面的"高级门禁系统":synchronized是Java内置关键字,使用简便但功能有限;而ReentrantLock作为完整类,提供更精细的控制能力。

基础用法示例

ReentrantLock lock = new ReentrantLock(); // 创建非公平锁
// ReentrantLock lock = new ReentrantLock(true); // 创建公平锁

public void criticalSection() {
    lock.lock();  // 获取锁
    try {
        // 临界区代码 - 共享资源访问逻辑
        System.out.println("线程" + Thread.currentThread().getName() + "正在操作共享资源");
    } finally {
        lock.unlock(); // 必须确保释放锁
    }
}

注意:lock.unlock()必须置于finally块,确保异常场景下锁仍能释放,避免死锁。

2 ReentrantLock的核心特性:超越基础锁机制

若将synchronized比作普通门锁,ReentrantLock则堪称智能指纹锁,满足多样复杂场景需求。以下对比表格清晰展示两者差异:

表:ReentrantLock与synchronized特性对比

特性 ReentrantLock synchronized
实现层面 API层面(JUC包) JVM层面(关键字)
锁的获取 可尝试、定时、可中断 仅支持阻塞等待
公平性 可选公平或非公平锁 仅非公平锁
条件队列 可绑定多个Condition 单一等待池
释放保证 需手动unlock() JVM自动释放

2.1 可重入性:递归调用的关键保障

可重入性是ReentrantLock的核心特性,允许同一线程多次获取同一把锁而不被阻塞。类比进入房间后需用同一钥匙开启内门,可重入锁确保线程不会被自身阻挡。

技术层面,可重入意味着:同一线程可重复获取同一锁而不引发阻塞,这对递归调用或嵌套方法需同一锁的场景至关重要。

public class RecursiveExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();  // 首次获取锁
        try {
            inner(); // 调用需同一锁的方法
            System.out.println("外部方法执行,锁重入次数: " + 
                lock.getHoldCount()); // 查看重入次数
        } finally {
            lock.unlock();
        }
    }

    public void inner() {
        lock.lock();  // 再次获取同一锁(重入)
        try {
            System.out.println("内部方法执行,当前重入次数: " + 
                lock.getHoldCount());
        } finally {
            lock.unlock();
        }
    }
}

若无可重入性,线程在inner()方法中尝试获取锁时将因已持有锁而阻塞,导致死锁。ReentrantLock通过内部计数器追踪重入次数:每次lock()计数器加1,每次unlock()减1,计数器归零时锁真正释放。

2.2 公平性与非公平性:资源分配策略选择

ReentrantLock提供公平与非公平两种锁模式,体现其策略灵活性:

  • 公平锁(new ReentrantLock(true)):遵循先来后到,保证等待最久线程优先获锁
  • 非公平锁(new ReentrantLock(false),默认):允许新线程插队,可能比早等待线程先获锁

性能权衡:公平锁保障公平性但性能较低(线程切换频繁);非公平锁虽不公平但吞吐量更高。多数场景下非公平锁更优,因减少线程切换开销。

2.3 尝试锁与可中断:灵活的资源获取

ReentrantLock提供多样锁获取方式,避免线程无限阻塞:

尝试锁tryLock):类似等电梯设时限制,超时则执行备选方案

public boolean tryIncrement(long timeout, TimeUnit unit) {
    try {
        if (lock.tryLock(timeout, unit)) { // 尝试指定时间内获锁
            try {
                counter++;
                return true;
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("获取锁超时,执行备用逻辑");
            return false;
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
}

可中断锁:等待锁过程可响应中断请求,如排队时接重要电话暂离

public void interruptibleLock() {
    try {
        lock.lockInterruptibly(); // 可中断获取锁
        try {
            while (!Thread.currentThread().isInterrupted()) {
                // 检查中断状态
            }
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        System.out.println("锁获取被中断,优雅退出");
        Thread.currentThread().interrupt();
    }
}

2.4 条件变量:精细化线程协调

synchronized配合wait()/notify()仅支持单一等待条件;ReentrantLock可创建多个条件变量(Condition),实现更精细线程协调,尤适用于生产者-消费者模型:

public class BoundedBuffer<T> {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();  // 非满条件
    private final Condition notEmpty = lock.newCondition(); // 非空条件
    private final Object[] items = new Object[100];
    private int putptr, takeptr, count;

    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await(); // 等待"非满"条件
            }
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal(); // 通知"非空"条件
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 等待"非空"条件
            }
            T x = (T) items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal(); // 通知"非满"条件
            return x;
        } finally {
            lock.unlock();
        }
    }
}

通过不同Condition,可精准控制线程唤醒,避免synchronizednotifyAll()引发的"惊群效应"。

3 ReentrantLock的实现原理:深入AQS核心机制

理解ReentrantLock工作原理需先掌握其基石:AQS(AbstractQueuedSynchronizer),即抽象队列同步器。AQS是Java并发包核心框架,ReentrantLock所有功能皆构建其上。

3.1 AQS:同步状态管理引擎

AQS作为同步状态管理器,维护三大关键组件:

  1. state(状态字段)volatile int变量,表示锁状态
    • ReentrantLock,state=0表示锁空闲
    • state>0表示锁占用,数值即重入次数
  2. 独占线程:记录当前持锁线程
  3. CLH队列:虚拟双向队列,管理等待线程

AQS采用模板方法模式,定义锁获取与释放骨架,具体逻辑由子类实现,使其成为强大同步框架。

3.2 加锁过程剖析:非公平锁为例

调用lock.lock()时,非公平锁执行流程如下:

// NonfairSync加锁过程
final void lock() {
    if (compareAndSetState(0, 1)) { // 1. 先尝试CAS快速获锁
        setExclusiveOwnerThread(Thread.currentThread()); // 成功:设当前线程为独占者
    } else {
        acquire(1); // 2. 失败:进入AQS获取流程
    }
}

// AQS的acquire方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) && // 3. 再次尝试获锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 4. 失败后入队阻塞
        selfInterrupt();
}

此过程类比医院挂号:

  1. 直接尝试(插队):新患者(线程)直接问窗口能否挂号(CAS操作)
  2. 快速成功:若无人挂号(state=0),直接成功免排队
  3. 正式排队:若窗口有人(state≠0),进入CLH队列
  4. 队列等待:轮到时再次尝试

非公平锁的tryAcquire实现

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 获取当前状态

    if (c == 0) { // 情况1:锁空闲
        if (compareAndSetState(0, acquires)) { // CAS尝试获取
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 情况2:重入
        int nextc = c + acquires; // 增加重入次数
        if (nextc < 0) // 溢出检查
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false; // 获取失败
}

3.3 释放锁过程:唤醒后继线程

释放锁过程侧重状态恢复与唤醒后继线程:

// ReentrantLock的unlock方法
public void unlock() {
    sync.release(1); // 委托给AQS的release方法
}

// 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;
}

// 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;
}

释放关键点:仅当重入次数归零时锁才真正释放,此时唤醒等待队列线程。

3.4 公平锁 vs 非公平锁的实现差异

公平与非公平锁核心差异体现在tryAcquire方法:

// 公平锁的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键区别:多了hasQueuedPredecessors()检查!
        if (!hasQueuedPredecessors() && // 检查是否有更早等待线程
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        // 重入逻辑同非公平锁
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors()方法是公平性守护者,检查同步队列中是否有更早等待线程。若有,当前线程不能插队,必须排队。

3.5 正确使用ReentrantLock的注意事项

尽管ReentrantLock功能强大,误用会导致严重问题。以下是关键实践要点:

1. lock()必须在try外部调用

// 正确写法
public void calculate() {
    lock.lock();  // lock()在try外部
    try {
        int result = 100 / 0; // 可能抛出异常
    } finally {
        lock.unlock();
    }
}

// 错误写法(可能导致异常信息被覆盖)
public void calculate() {
    try {
        lock.lock();  // 错误:lock()在try内部
        int result = 100 / 0;
    } finally {
        lock.unlock();
    }
}

2. 必须使用try-finally确保锁释放

public void riskyMethod() {
    lock.lock();
    try {
        dangerousOperation(); // 可能抛出异常
    } finally {
        lock.unlock(); // 保证锁释放
    }
}

3. 避免lock()与try间插入代码

public void problematicMethod() {
    lock.lock();
    int num = 1 / 0; // 危险:加锁后try前可能异常!
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
}

遵循这些实践可规避常见陷阱,确保ReentrantLock正确使用。

4 实战应用与总结

4.1 实战场景举例

场景1:高性能计数器

public class HighPerformanceCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    // 使用tryLock实现非阻塞版本
    public boolean tryIncrement() {
        if (lock.tryLock()) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
}

场景2:简单阻塞队列

public class SimpleBlockingQueue<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private final int capacity;

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // 等待"非满"条件
            }
            queue.offer(item);
            notEmpty.signal(); // 通知"非空"条件
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 等待"非空"条件
            }
            T item = queue.poll();
            notFull.signal(); // 通知"非满"条件
            return item;
        } finally {
            lock.unlock();
        }
    }
}

4.2 总结与选型建议

ReentrantLock是Java并发编程关键工具,基于AQS实现高效可重入锁机制。通过源码分析,我们掌握:

  • 全局结构:Sync、NonfairSync和FairSync协同工作
  • 核心逻辑:state管理锁状态,CAS保证原子性
  • 生命周期:初次锁依赖CAS,重入更新state,释放递减state
  • 公平性:非公平锁高吞吐,公平锁防饥饿

选型建议

  • 首选synchronized:简单场景,无需ReentrantLock高级功能时
  • 需高级功能时选ReentrantLock:可中断、超时、公平锁、多条件变量等复杂场景
  • 慎用公平锁:公平锁有性能开销,除非必要(如防饥饿),否则用非公平锁
  • 确保正确释放:unlock()必须置于finally块,避免死锁

ReentrantLock提供比synchronized更精细锁控制,是处理复杂并发场景的利器。深入理解其实现原理,有助于编写高效、可靠并发程序。

参考资料

  1. Java并发编程实战
  2. Java并发包源码分析
  3. AQS框架技术文档
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-1 14:12 , Processed in 0.058084 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表