Skip to content

JUC ReentrantLock

本文介绍java.util.concurrent.locks.ReentrantLock类的使用及其特性。

1. 简介与基本使用

ReentrantLock是JUC提供的锁,相较于synchronized有如下特点:

  • 可中断:当t1线程由于竞争锁失败进入阻塞队列时,另一个线程可以调用interrupt()方法打断t1线程,使其恢复运行;
  • 可以设置超时时间:当t1线程由于竞争锁失败进入阻塞队列时,可以设置在阻塞队列中最长等待多长时间,如果超时仍没有获取到锁,则不再等待;
  • 可以设置为公平锁:公平锁 (Fair Lock)是一种锁机制,它保证了线程获取锁的顺序是严格按照线程请求锁的顺序来决定的。简单来说,就是先到先得:等待时间最长的线程将优先获得锁。
  • 支持多个条件变量:synchronized原理的Monitor对象支持一个WaitSet,而ReentrantLock可以支持多个WaitSet,所以某个线程拿到锁之后,由于不满足执行条件,放弃锁进入其中一个条件变量(WaitSet)等待,之后其他的线程可以精确唤醒某个条件变量中等待的线程。

synchronized一样,ReentrantLock也支持可重入。

下面的例子演示了ReentrantLock的基本使用:

java
public static void main(String[] args) {
  	// 创建锁对象
    ReentrantLock reentrantLock = new ReentrantLock();
  
    Thread t1 = new Thread(() -> {
        // 调用lock()获取锁
        reentrantLock.lock();
        try {
          	// 同步代码块
            log.info("获取到锁");
        } finally {
            // 在finally块中保证释放锁逻辑一定执行
            reentrantLock.unlock();
        }
    }, "t1");

    Thread t2 = new Thread(() -> {
        reentrantLock.lock();
        try {
            log.info("获取到锁");
        } finally {
            reentrantLock.unlock();
        }
    }, "t2");

    t1.start();
    t2.start();
}

2. 可重入

ReentrantLock支持可重入,即某个线程获取了锁之后,可以再次获取同一把锁:

java
public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();

    reentrantLock.lock();
    try {

        log.info("第一次获取到锁");
        reentrantLock.lock();
        try {
            log.info("第二次获取到锁");
        }finally {
            reentrantLock.unlock();
        }

    }finally {
        reentrantLock.unlock();
    }
}

结果如下:

txt
14:17:07.103 [main] INFO  : 第一次获取到锁
14:17:07.104 [main] INFO  : 第二次获取到锁

可以证明ReentrantLock是支持可重入的。

3. 可中断

可中断是指当线程由于竞争锁失败进入阻塞队列时,另一个线程可以调用interrupt()方法打断该线程,使其恢复运行,需要注意的是,如果要使该线程是可中断的,需要在获取锁的时候,调用lockInterruptibly()方法

java
public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();
    new Thread(()->{
        reentrantLock.lock();
        try {
            log.info("获取到锁");
            Sleeper.sleep(5 * 1000);  // 睡眠5秒
        }finally {
            reentrantLock.unlock();
        }
    },"t1").start();

    Sleeper.sleep(1000);
    Thread t2 = new Thread(() -> {
        log.info("启动...");
        try {
            reentrantLock.lockInterruptibly();
        } catch (InterruptedException e) {
            // 在阻塞等待锁期间被中断,抛出异常,此时没有获取到锁,可以直接返回
            log.info("被中断,没有获取到锁,直接返回");
            return;
        }

        try {
            log.info("获取到锁");
        }finally {
            reentrantLock.unlock();
        }
    }, "t2");
    t2.start();

    Sleeper.sleep(1000);
    log.info("主线程调用interrupt()打断t2线程");
    t2.interrupt();  // 主线程调用interrupt()打断t2线程

}

结果如下:

txt
14:25:17.145 [t1] INFO  : 获取到锁
14:25:18.147 [t2] INFO  : 启动...
14:25:19.151 [main] INFO  : 主线程调用interrupt()打断t2线程
14:25:19.152 [t2] INFO  : 被中断,没有获取到锁,直接返回

首先t1线程获取到锁,并且在持有锁期间t2线程启动,并且t2线程也需要获取锁,由于获取失败进入阻塞状态。主线程调用interrupt()打断t2线程,使其恢复运行。

4. 超时等待

4.1 基本使用

ReentrantLock可以设置阻塞超时时间,主要由以下方法实现:

  • boolean tryLock():线程试图获取锁,如果可以立即获取到锁,则获取到锁并返回true,反之则返回false
  • boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:线程试图获取锁,如果不能立即获取到锁,则进入阻塞队列中等待,并且等待时间不超过设定时间,如果在设定时间内获取到锁,则返回true,反之则返回false。允许在等待期间被打断。

简单示例如下:

java
public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();
    new Thread(()->{
        reentrantLock.lock();
        try {
            log.info("获取到锁");
            Sleeper.sleep(5 * 1000);  // 睡眠5秒
        }finally {
            reentrantLock.unlock();
        }
    },"t1").start();

    Sleeper.sleep(1000);
    Thread t2 = new Thread(() -> {
        log.info("启动...");
        try {
            boolean hasLock = reentrantLock.tryLock(1, TimeUnit.SECONDS);
            if (!hasLock){
                log.info("超时等待后仍没有获取到锁,直接返回");
                return;
            }
        } catch (InterruptedException e) {
            // 在阻塞等待锁期间被中断,抛出异常,此时没有获取到锁,可以直接返回
            log.info("被中断,没有获取到锁,直接返回");
            return;
        }

        try {
            log.info("获取到锁");
        }finally {
            reentrantLock.unlock();
        }
    }, "t2");
    t2.start();

}

结果如下:

txt
14:29:36.074 [t1] INFO  : 获取到锁
14:29:37.079 [t2] INFO  : 启动...
14:29:38.080 [t2] INFO  : 超时等待后仍没有获取到锁,直接返回

线程t2由于获取锁失败,并且等待1秒后仍然没有获取到锁,则放弃等待,直接返回。

4.2 解决哲学家就餐问题

我们可以利用ReentrantLock的超时等待特性解决哲学家就餐问题:

java
public static void main(String[] args) {
    Chopstick c1 = new Chopstick(1);
    Chopstick c2 = new Chopstick(2);
    Chopstick c3 = new Chopstick(3);
    Chopstick c4 = new Chopstick(4);
    Chopstick c5 = new Chopstick(5);

    new Philosopher("阿基米德",c1, c5).start();
    new Philosopher("赫拉克利特",c5, c4).start();
    new Philosopher("亚里士多德",c4, c3).start();
    new Philosopher("柏拉图",c3, c2).start();
    new Philosopher("苏格拉底",c2, c1).start();
}
java
@Slf4j
class Philosopher extends Thread{
    private Chopstick left;
    private Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right){
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true){
            eat();
        }
    }

    public void eat(){
        try {
            boolean hasLeftLock = left.tryLock(1, TimeUnit.SECONDS);
            if(!hasLeftLock){
                log.info("未获取左边的筷子");
                return;
            }
        } catch (InterruptedException e) {
            return;
        }

        try {
            log.info("获取左边的筷子");
            try {
                boolean hasRightLock = right.tryLock(1, TimeUnit.SECONDS);
                if(!hasRightLock){
                    log.info("未获取右边的筷子,放弃左边的筷子");
                    return;
                }
            } catch (InterruptedException e) {
                return;
            }

            try {
                log.info("吃饭");
                Sleeper.sleep(1000);
            }finally {
                right.unlock();
            }

        }finally {
            left.unlock();
        }
    }
}
java
class Chopstick extends ReentrantLock{
    private int id;

    public Chopstick(int id){
        super();
        this.id = id;
    }
}

结果发现程序会一直执行下去,不会发生死锁。

基本思想是当获取右边的筷子失败时,就放弃掉左边的筷子,好让其他人可以正常获取筷子执行下去吃饭。

5. 设置公平锁

ReentrantLock的构造方法支持传入一个布尔值,用于设置是否为公平锁:

java
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

具体的内容可以查看源码分析。

6. 多个条件变量

ReentrantLock可以用来创建多个条件变量,所谓条件变量,就是synchronized中Monitor对象的WaitSet,当线程拿到锁之后,如果发现执行条件不满足,则放弃锁进入某个条件变量(WaitSet)中进行等待,其他线程后续可以精确唤醒某个条件变量中等待的线程。

以现实生活中的例子说明条件变量,机场会提供多个候机室(多个条件变量),当旅客(线程)发现没有到登机时间(没有达到执行条件),就会进入特定的候机室等待。当工作人员(也是线程)发现某架航班可以登机时,到特定的候机室通知旅客(唤醒等待中的线程),而不用唤醒其他候机室的旅客。

下面以一个简单的例子说明条件变量的使用:

  • 首先创建一个ReentrantLock锁,并且在该锁的基础上创建两个条件变量,调用reentrantLock.newCondition()方法;
  • 然后创建两个线程t1和t2,当发现flag1为false时,调用condition.await();进入条件变量1中等待;
  • 创建线程t3,当发现flag2为false时,调用condition.await();进入条件变量2中等待;
  • 一秒后,主线程修改flag1为true,并且调用condition1.signalAll();唤醒在条件变量1中等待的线程,使他们继续恢复运行;注意,也可以调用condition1.signal()随机唤醒一个在条件变量1中等待的线程。
  • 由于t3线程没有被唤醒,所以始终被阻塞,导致程序永远无法结束;
java
static ReentrantLock reentrantLock = new ReentrantLock();
static Condition condition1 = reentrantLock.newCondition();
static Condition condition2 = reentrantLock.newCondition();

static boolean flag1 = false;
static boolean flag2 = false;

public static void main(String[] args) {
    for (int i = 1; i <= 2; i++) {
        Thread t1 = new Thread(() -> {
            log.info("开始执行");
            reentrantLock.lock();
            try {
                while (!flag1) {
                    try {
                        log.info("不满足条件,放弃锁,进入条件变量1等待");
                        condition1.await();
                    } catch (InterruptedException e) {

                    }
                }

                log.info("满足条件,继续执行");

            } finally {
                reentrantLock.unlock();
            }
        }, "t" + i);
        t1.start();
    }

    Thread t3 = new Thread(() -> {
        log.info("开始执行");
        reentrantLock.lock();
        try {
            while (!flag2) {
                try {
                    log.info("不满足条件,放弃锁,进入条件变量2等待");
                    condition2.await();
                } catch (InterruptedException e) {

                }
            }

            log.info("满足条件,继续执行");

        } finally {
            reentrantLock.unlock();
        }
    }, "t3");
    t3.start();

    Sleeper.sleep(1000);

    reentrantLock.lock();
    try {
        log.info("唤醒条件变量中的线程继续执行");
        flag1 = true;
        condition1.signalAll();
    }finally {
        reentrantLock.unlock();
    }
}

结果如下:

txt
15:40:58.515 [t2] INFO  : 开始执行
15:40:58.515 [t1] INFO  : 开始执行
15:40:58.515 [t3] INFO  : 开始执行
15:40:58.517 [t2] INFO  : 不满足条件,放弃锁,进入条件变量1等待
15:40:58.517 [t1] INFO  : 不满足条件,放弃锁,进入条件变量1等待
15:40:58.517 [t3] INFO  : 不满足条件,放弃锁,进入条件变量2等待
15:40:59.517 [main] INFO  : 唤醒条件变量中的线程继续执行
15:40:59.519 [t2] INFO  : 满足条件,继续执行
15:40:59.519 [t1] INFO  : 满足条件,继续执行

注意事项:

  • 调用await()/signal()/signalAll()前需要先获得锁;
  • await()会释放锁,并进行等待,也可以调用await(long time, TimeUnit unit)进行限时等待,当等待超时后,则线程自动醒来竞争锁;
  • 被唤醒后的线程需要重新竞争锁,竞争成功后,从await()后继续执行,竞争失败则进入阻塞队列;