在 Java 并发编程中,提到锁,很多人第一反应是 synchronized。它简单、好用,而且是 JVM 层面的关键字。

但是在一些更复杂的并发场景下,synchronized 就显得不够灵活了。比如:

我想尝试获取锁,拿不到就直接返回
我想最多等待 3 秒,超过时间就放弃
我想让等待锁的线程可以被中断
我想实现公平锁
我想有多个等待条件队列

这时候,就需要用到 JDK 并发包提供的锁:

java.util.concurrent.locks.ReentrantLock

本文就系统介绍一下 ReentrantLock

一、什么是 ReentrantLock?

ReentrantLock 中文叫:可重入锁

它是 JDK 提供的一种显式锁,位于:java.util.concurrent.locks.ReentrantLock

它的基本作用和 synchronized 类似,都是为了保证多线程访问共享资源时的线程安全。

最基本的使用方式如下:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void test() {
        lock.lock(); // 加锁
        try {
            System.out.println("执行业务逻辑");
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

这里最关键的是:

lock.lock();  表示获取锁。

lock.unlock();  表示释放锁。

注意:使用 ReentrantLock 时,一定要把 unlock() 放在 finally 中。

否则业务代码一旦抛异常,锁可能无法释放,其他线程就会一直等待。

二、为什么叫“可重入锁”?

所谓可重入,就是:同一个线程已经拿到锁之后,还可以再次获取同一把锁,不会把自己阻塞住。

举个例子:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            System.out.println("methodA");
            methodB();
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            System.out.println("methodB");
        } finally {
            lock.unlock();
        }
    }
}

执行流程如下:

线程 T1 调用 methodA()
线程 T1 第一次获取锁,state = 1
线程 T1 在 methodA 中调用 methodB()
线程 T1 第二次获取同一把锁,state = 2
methodB 执行完,释放一次锁,state = 1
methodA 执行完,再释放一次锁,state = 0

同一个线程可以多次进入同一把锁,这就是可重入。

但是要注意:加锁几次,就必须解锁几次。

三、ReentrantLock 的核心特性

reentrantLock 主要有以下几个特点:

1. 可重入
2. 可手动加锁和释放锁
3. 支持公平锁和非公平锁
4. 支持 tryLock 尝试加锁
5. 支持超时等待
6. 支持可中断等待
7. 支持多个 Condition 条件队列
8. 底层基于 AQS 实现

四、ReentrantLock 基本使用

1. 普通加锁

import java.util.concurrent.locks.ReentrantLock;

public class Counter {

    private int count = 0;

    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();

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

    public int getCount() {
        return count;
    }
}

这里 count++ 不是原子操作,它大概可以分成三步:

1. 读取 count
2. count + 1
3. 写回 count

如果多个线程同时执行,可能出现线程安全问题。

使用 ReentrantLock 后,同一时间只有一个线程可以执行 count++

五、公平锁和非公平锁

1. 非公平锁

默认创建方式:

ReentrantLock lock = new ReentrantLock();

等价于  ReentrantLock lock = new ReentrantLock(false);

非公平锁的特点是:新来的线程可以直接抢锁,不一定按照排队顺序获取锁。

比如现在 AQS 队列里已经有线程 B、C 在等待:

head -> 线程B -> 线程C

此时锁刚好释放,线程 D 刚好来了。

在非公平锁中,线程 D 可以直接尝试 CAS 抢锁。

如果抢成功:

线程 D 插队成功
线程 B 继续等待

非公平锁的优点:

性能高
吞吐量高
线程切换较少

缺点:

不够公平
可能导致某些线程等待时间较长

2. 公平锁

创建公平锁:

ReentrantLock lock = new ReentrantLock(true);

公平锁的特点是:线程获取锁时要按照队列顺序,先来的线程先获取锁。

也就是说,如果队列中已经有人等待,新来的线程不能插队。

底层会判断:hasQueuedPredecessors()

当前线程前面是否有其他线程在排队?

如果前面有人,就不能直接抢锁。

公平锁的优点:

更公平
不容易出现线程长期等待

缺点:

性能比非公平锁低
线程上下文切换更多
吞吐量较低

实际开发中,大多数情况下使用默认的非公平锁即可。

六、tryLock:尝试获取锁

if (lock.tryLock()) {
    try {
        System.out.println("获取锁成功,执行业务");
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("获取锁失败,直接返回");
}

tryLock() 的含义是:

能拿到锁就执行
拿不到锁就立即返回 false
不会一直阻塞等待

这个在实际业务中非常有用。

比如防止重复提交:

public String submitOrder(Long userId) {
    boolean locked = lock.tryLock();

    if (!locked) {
        return "操作太频繁,请稍后再试";
    }

    try {
        // 创建订单
        return "下单成功";
    } finally {
        lock.unlock();
    }
}

如果多个请求同时进来,只有一个请求能拿到锁,其他请求直接返回,避免一直等待。

2. 等待指定时间

tryLock 还可以指定等待时间:

if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        System.out.println("3 秒内获取锁成功");
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("等待 3 秒仍然没有获取锁,直接失败");
}

完整示例:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void doBusiness() {
        boolean locked = false;

        try {
            locked = lock.tryLock(3, TimeUnit.SECONDS);

            if (!locked) {
                System.out.println("系统繁忙,请稍后再试");
                return;
            }

            System.out.println("获取锁成功,执行业务逻辑");

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("线程被中断");
        } finally {
            if (locked) {
                lock.unlock();
            }
        }
    }
}

七、lockInterruptibly:可中断加锁

普通的 lock() 如果获取不到锁,会一直等待。

lock.lock();

如果线程在等待锁的过程中被中断,它不会立刻响应中断。

lockInterruptibly() 支持在等待锁的过程中响应中断。

示例:

import java.util.concurrent.locks.ReentrantLock;

public class InterruptLockDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void doWork() {
        try {
            lock.lockInterruptibly();

            try {
                System.out.println("获取锁成功,执行业务");
            } finally {
                lock.unlock();
            }

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("等待锁时被中断");
        }
    }
}

适合场景:

任务可以取消
线程等待时间不可控
不希望线程一直卡死在获取锁的过程

八、Condition 条件队列

ReentrantLock 还可以配合 Condition 使用。

Condition 类似于 synchronized 中的:

wait()
notify()
notifyAll()

但是 Condition 更灵活,因为一把锁可以创建多个条件队列。

1. 基本用法

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {

    private final ReentrantLock lock = new ReentrantLock();

    private final Condition condition = lock.newCondition();

    public void awaitMethod() throws InterruptedException {
        lock.lock();

        try {
            System.out.println("线程进入等待");
            condition.await();
            System.out.println("线程被唤醒");
        } finally {
            lock.unlock();
        }
    }

    public void signalMethod() {
        lock.lock();

        try {
            System.out.println("唤醒等待线程");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

注意:condition.await();

会做两件事:

1. 当前线程释放锁
2. 当前线程进入 Condition 等待队列

当其他线程调用:

condition.signal();

等待线程会被唤醒,然后重新进入 AQS 队列竞争锁。

2. Condition 和 wait/notify 的区别

九、ReentrantLock 和 synchronized 的区别

十、ReentrantLock 底层原理

ReentrantLock 底层是基于 AQS 实现的。

AQS 全称:AbstractQueuedSynchronizer(抽象队列同步器)

AQS 的核心可以总结成一句话: AQS = state + CAS + FIFO 队列 + park/unpark

也就是:

state:表示锁状态
CAS:保证修改 state 的原子性
FIFO 队列:保存抢锁失败的线程
park:阻塞线程
unpark:唤醒线程

十一、state 在 ReentrantLock 中的含义

AQS 中有一个核心变量:

private volatile int state;

ReentrantLock 中,state 表示锁被重入的次数。

state = 0:锁没有被线程持有
state = 1:锁被某个线程持有一次
state = 2:同一个线程重入两次

例如:

lock.lock();   // state = 1
lock.lock();   // state = 2

lock.unlock(); // state = 1
lock.unlock(); // state = 0

所以,ReentrantLock 可重入的本质就是:

同一个线程再次获取锁时,state 会累加。

十二、ReentrantLock 加锁流程

以非公平锁为例。

当线程调用:

lock.lock();

底层大致流程如下:

1. 线程先尝试 CAS 把 state 从 0 改成 1
2. 如果修改成功,说明获取锁成功
3. 设置当前线程为锁持有者
4. 如果 state 不为 0,判断持有锁的线程是不是当前线程
5. 如果是当前线程,说明是重入,state + 1
6. 如果不是当前线程,说明获取锁失败
7. 当前线程被封装成 Node 节点进入 AQS 队列
8. 当前线程通过 LockSupport.park() 阻塞

伪代码理解:

if (state == 0) {
    if (compareAndSetState(0, 1)) {
        owner = 当前线程;
        return;
    }
}

if (owner == 当前线程) {
    state++;
    return;
}

进入 AQS 队列等待;

队列大概这样:

head -> 线程B -> 线程C -> 线程D

抢锁失败的线程会排队等待。

十三、ReentrantLock 解锁流程

当线程调用:

lock.unlock();

底层大致流程如下:

1. 判断当前线程是不是锁持有者
2. 如果不是,抛出 IllegalMonitorStateException
3. 如果是,state - 1
4. 如果 state 不等于 0,说明只是释放了一层重入锁
5. 如果 state 等于 0,说明锁彻底释放
6. 清空锁持有者
7. 唤醒 AQS 队列中的后继线程

伪代码理解:

if (owner != 当前线程) {
    throw new IllegalMonitorStateException();
}

state--;

if (state == 0) {
    owner = null;
    唤醒下一个等待线程;
}

所以:只有当 state 减到 0,锁才真正释放。

十四、AQS 队列和线程阻塞唤醒

当线程获取锁失败时,会被包装成一个 Node 节点,加入 AQS 队列。

head -> Node(Thread-B) -> Node(Thread-C) -> Node(Thread-D)

每个 Node 大概包含:

thread:当前线程
prev:前一个节点
next:后一个节点
waitStatus:等待状态

然后线程会被阻塞:

LockSupport.park();

当持锁线程释放锁时,会唤醒后继线程:

LockSupport.unpark(thread);

注意:

被唤醒的线程不是直接获得锁,而是重新尝试获取锁。

也就是说:

唤醒 ≠ 拿到锁
唤醒之后还要竞争锁

十五、ReentrantLock 的常见使用场景

1. 防止重复提交

public class SubmitService {

    private final ReentrantLock lock = new ReentrantLock();

    public String submit() {
        if (!lock.tryLock()) {
            return "请勿重复提交";
        }

        try {
            // 执行业务逻辑
            return "提交成功";
        } finally {
            lock.unlock();
        }
    }
}

2. 限时等待资源

public String handle() {
    boolean locked = false;

    try {
        locked = lock.tryLock(2, TimeUnit.SECONDS);

        if (!locked) {
            return "系统繁忙,请稍后再试";
        }

        return "处理成功";

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return "请求被中断";
    } finally {
        if (locked) {
            lock.unlock();
        }
    }
}

4. 需要公平性的业务场景

比如某些资源分配系统:

ReentrantLock lock = new ReentrantLock(true);

不过要注意,公平锁会降低吞吐量,一般业务不建议随便使用。

总结:

ReentrantLock 底层通过 AQS 的 state 变量记录锁的重入次数。当 state 为 0 时表示锁没有被持有,线程通过 CAS 把 state 从 0 改成 1 后获取锁成功。如果同一个线程再次获取锁,会判断当前线程是否是锁持有者,如果是,就把 state 加 1。释放锁时 state 减 1,只有当 state 减到 0 时,锁才真正释放。

ReentrantLock 底层通过 AQS 的 state 变量记录锁的重入次数。当 state 为 0 时表示锁没有被持有,线程通过 CAS 把 state 从 0 改成 1 后获取锁成功。如果同一个线程再次获取锁,会判断当前线程是否是锁持有者,如果是,就把 state 加 1。释放锁时 state 减 1,只有当 state 减到 0 时,锁才真正释放。

synchronized 是 Java 关键字,底层由 JVM Monitor 实现,使用简单,不需要手动释放锁。ReentrantLock 是 JDK 提供的类,底层基于 AQS 实现,需要手动 lock 和 unlock。相比 synchronized,ReentrantLock 更灵活,支持公平锁、tryLock、超时等待、可中断等待和 Condition 条件队列。但是使用时必须在 finally 中释放锁,否则可能导致死锁。

公平锁是线程获取锁时按照 AQS 队列顺序来,先等待的线程优先获取锁;非公平锁允许新来的线程直接尝试 CAS 抢锁,可能出现插队。ReentrantLock 默认是非公平锁,因为非公平锁减少了线程切换,性能更高。公平锁更公平,但吞吐量相对低。

ReentrantLock 底层基于 AQS 实现。AQS 内部维护了一个 volatile 的 state 变量和一个 FIFO 等待队列。线程加锁时会先通过 CAS 修改 state,如果修改成功则获取锁;如果失败,就封装成 Node 节点加入 AQS 队列,并通过 LockSupport.park 阻塞。释放锁时,state 减 1,当 state 为 0 时表示锁释放,然后通过 LockSupport.unpark 唤醒队列中的后继线程。

普通同步场景优先 synchronized;
复杂并发控制场景可以使用 ReentrantLock。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐