Appearance
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()后继续执行,竞争失败则进入阻塞队列;