ReentrantLock 用法详解

2022-06-27 12:44:23 浏览数 (3)

1. 概述

此前的文章中我们介绍了 synchronized 锁的使用及实现原理: synchronized 的使用及实现原理 文中,我们看到,jdk1.6 对 synchronized 锁进行了一系列的优化,使得我们再也不用为 synchronized 锁的性能担忧,在此之前,synchronized 锁因为其性能问题是很少被使用的,那时,最常用的锁结构就是今天我们要介绍的 ReentrantLock 锁。 虽然时至今日,优化后的 synchronized 锁的性能已经与 ReentrantLock 接近,但 ReentrantLock 仍然具备着 synchronized 所不具备的很多优势: 1. 吞吐量更高、执行效率更高 2. 支持公平锁、非公平锁两种模式 3. 更加灵活精准的等待与唤醒 4. 可中断锁、轮询锁 5. 可伸缩性强 6. 支持条件变量 因此,在高度竞争的并发环境下,以及较为复杂的使用场景中,ReentrantLock 都是 synchronized 的有力替代品。

本文,我们就来介绍一下 ReentrantLock 的用法,下一篇文章中,我们详细的剖开源码解析 ReentrantLock 的实现原理。

2. ReentrantLock 的基本使用 — 加锁和解锁

ReentrantLock 类提供了最基本的加锁和解锁方法:

代码语言:javascript复制
public void lock();
public void unlock();

我们通过最基本的加锁方法实现上一篇日志中提到的自增方法锁。

代码语言:javascript复制
class Counter {
    private static int counter = 0;
    private static ReentrantLock lock = new ReentrantLock();

    public static int getCounter() {
        return counter;
    }

    public static void increase() {
        try {
            lock.lock();
            counter  ;
        } finally {
            lock.unlock();
        }
    }

    private Counter() {};
}

这个方法保证了线程安全,他和 synchronized 关键字实现了相同的效果:

代码语言:javascript复制
class Counter {
    private static int counter = 0;

    public static int getCounter() {
        return counter;
    }

    public synchronized static void increase() {
        counter  ;
    }

    private Counter() {};
}

显然,synchronized 关键字的实现更为简洁和清晰,同时,如果 ReentrantLock 忘记调用 unlock 方法将会造成死锁,这是必须要注意的一点。 因此,如果仅仅是想要进行上面代码中这样的加锁和解锁,synchronized 还是最好的选择。

3. 公平模式与非公平模式

使用 synchronized 锁是不保证等待的线程获取到锁的顺序的,这就是非公平锁,除了默认的非公平锁构造方法外,ReentrantLock 还提供了一个带有 boolean 参数的构造方法:

代码语言:javascript复制
public ReentrantLock(boolean fair);

如果传入参数为 true,则会创建公平锁,所谓的公平锁,就是保证了先进入等待的线程一定先获取到锁。

可以通过 isFair 方法查询 ReentrantLock 对象是否是公平锁:

代码语言:javascript复制
public final boolean isFair();

4. 非阻塞式锁、时间限制锁与可中断锁

ReentrantLock 提供了 tryLock 方法与 lockInterruptibly 方法用来实现非阻塞式锁、时间限制锁与可中断锁。

代码语言:javascript复制
public boolean tryLock()    // 尝试获取锁,立即返回获取结果
public boolean tryLock(long timeout, TimeUnit unit)    // 尝试获取锁,最多等待 timeout 时长
public void lockInterruptibly()    // 可中断锁,调用线程 interrupt 方法,则锁方法抛出 InterruptedException

以上三种锁方式,synchronized 都是无法实现的,正如我们上一篇日志中所提到,interrupt 方法是不会中止正在等待获取 synchronized 锁的线程的。

5. Condition 与线程等待

Object 类提供了只能在 synchronized 代码块中使用的 wait、notify、notifyAll,ReentrantLock 也拥有类似但更为强大的等待和唤醒机制,这就是通过 Condition 对象唤醒的。

5.1. Condition

通过 newCondition 方法,可以创建出 Condition 对象。

代码语言:javascript复制
public Condition newCondition();

Condition 接口提供了如下的方法:

代码语言:javascript复制
void await();                                // 可被中断的等待
boolean await(long time, TimeUnit unit);    // 最多等待 time 时长的可中断等待
long awaitNanos(long nanosTimeout);            // 最多等待 nanosTimeout 毫秒的可中断等待
boolean awaitUntil(Date deadline);            // 等待直到指定时间的可中断等待
void awaitUninterruptibly();                // 不可中断的等待
void signal();                                // 唤醒一个线程
void signalAll();                            // 唤醒所有等待中的线程

上面的五个等待方法中,除了 awaitUninterruptibly 方法,其他四个都可以被 interrupt 方法中断,而 signal 和 signalAll 方法可以中断上述所有等待方法。 但是,signal 和 signalAll 方法只能唤醒通过当前 Condition 对象调用过等待方法的线程。 基于上述特性,我们可以精准的控制让某个指定的线程被唤醒,而 Object 的 notify、notifyAll 方法的唤醒则是随机的,同一个 ReentrantLock 每次调用 newCondition 方法都将获得不同的 Condition 对象。

6. 强大而丰富的查询接口

除了上述强大的加锁与等待、唤醒接口外,ReentrantLock 还提供了丰富而强大的查询接口,让你了解到锁相关的各种情况:

代码语言:javascript复制
int getHoldCount();        // 获取当前线程持有该锁的次数
boolean isHeldByCurrentThread();    // 判断当前线程是否持有该锁
boolean isLocked();        // 获取锁状态是否为加锁状态
boolean isFair();        // 当前锁是否是公平锁
Thread getOwner();        // 获取持有锁的线程
boolean hasQueuedThreads();            // 判断当前锁是否有线程在等待
boolean hasQueuedThread(Thread thread);    // 判断指定线程是否在等待该锁
int getQueueLength();    // 获取正在等待该锁的线程数量
boolean hasWaiters(Condition condition);    // 判断是否有线程等待在该 Condition 对象上
int getWaitQueueLength(Condition condition);    // 获取等待在该 Condition 对象的线程数

0 人点赞