基本介绍 ReentrantLock 相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestReentrantLock
* @projectName hm-juc-codes
* @description: 测试可重入锁
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 17:23
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestReentrantLock")
public class TestReentrantLock {/**
* 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
* 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
*/static ReentrantLock lock = new ReentrantLock();
public static void method01() {lock.lock();
try {
log.debug("execute method01");
method02();
} finally {
// 释放锁
lock.unlock();
}}private static void method02() {
lock.lock();
try {
log.debug("execute method02");
method03();
} finally {
// 释放锁
lock.unlock();
}
}private static void method03() {lock.lock();
try {
log.debug("execute method03");
} finally {
// 释放锁
lock.unlock();
}
}public static void main(String[] args) {
method01();
}}
文章图片
【Java系列|ReentrantLock 可重入锁】可以看到执行结果, 当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行
可打断 可打断就是, 当前线程
t1
在等待锁的时候, 可以被其他的线程t2
使用 t1.interrupt()
方法打断.lock.lockInterruptibly()
尝试获取锁, 并且这个等待锁是可以被打断的/**
* 除非当前线程被中断,否则获取锁。
* 如果没有被另一个线程持有,则获取锁并立即返回,将锁持有计数设置为 1。
* 如果当前线程已经持有这个锁,那么持有计数加一并且方法立即返回。
* 如果锁被另一个线程持有,那么当前线程将被禁用以用于线程调度目的并处于休眠状态,直到发生以下两种情况之一:
* 锁被当前线程获取;或者
* 其他一些线程中断当前线程。
* 如果当前线程获取了锁,则锁持有计数设置为 1。
* 如果当前线程:
* 在进入此方法时设置其中断状态;或者
* 在获取锁时被中断,
* 然后抛出InterruptedException并清除当前线程的中断状态。
* 在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁。
* 抛出:
* InterruptedException – 如果当前线程被中断
*/
lock.lockInterruptibly();
我们通过下面的代码去演示
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
/**
* @author 王天赐
* @title: TestLockInterruptibly
* @projectName hm-juc-codes
* @description: 测试可打断
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 14:01
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestLockInterruptibly")
public class TestLockInterruptibly {private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {Thread t1 = new Thread(() -> {
try {
// 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常
// 如果有竞争就进入阻塞队列
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取锁的过程中被打断");
return;
}try {
log.debug("获得锁... ");
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {try {
lock.lock();
log.debug("获取锁 ... ");
sleep(1);
log.debug("打断t1线程的等待锁的过程!");
t1.interrupt();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "t2");
t2.start();
t1.start();
}
}
文章图片
我们可以看到 t2 线程先获取锁, 然后t1线程在等待锁获取的过程中被t2线程打断
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
修改下上面的代码
Thread t1 = new Thread(() -> {
try {
// 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常
// 如果有竞争就进入阻塞队列
// lock.lockInterruptibly();
lock.lock();
} catch (Exception e) {
e.printStackTrace();
log.debug("没有获得锁, 返回");
return;
}try {
log.debug("获得锁... ");
} finally {
lock.unlock();
}
}, "t1");
文章图片
可以看到, 即使执行了
interrupt
但是实际上还是没有打断锁超时 锁超时就是, 如果无法获取锁, 不仅如此阻塞队列, 直接结束
// 仅当调用时没有被另一个线程持有时才获取锁。
// 如果没有被另一个线程持有,则获取锁,并立即返回值为true ,将锁持有计数设置为 1。
// 即使此锁已设置为使用公平排序策略,调用tryLock()也会立即获取该锁(如果可用),无论其他线程当前是否正在等待该锁。
// 这种“闯入”行为在某些情况下可能很有用,即使它破坏了公平性。
// 如果您想尊重此锁的公平设置,请使用几乎等效的tryLock(0, TimeUnit.SECONDS) (它也检测中断)。
// 如果当前线程已经持有这个锁,那么持有计数加一并且该方法返回true 。
// 如果锁被另一个线程持有,则此方法将立即返回值为false 。
// 返回:true锁是空闲的并且被当前线程获取,或者锁已经被当前线程持有,则返回 true;否则falsepublic boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestTimeOutLock
* @projectName hm-juc-codes
* @description: 测试锁超时
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 15:29
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestTimeOutLock")
public class TestTimeOutLock {private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
if (!lock.tryLock()) {
log.debug("获取锁失败!, 直接跑路");
return;
}try {
log.debug("获得了锁 !");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获取锁");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁...");
}
}
}
文章图片
可以看到这里获取锁失败直接就退出了 , 我们也可以使用
tryLock(long, TimeUnit)
方法来设置尝试的时间package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestTimeOutLock
* @projectName hm-juc-codes
* @description: 测试锁超时
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 15:29
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestTimeOutLock")
public class TestTimeOutLock {private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("获取锁失败!, 直接跑路");
return;
}
log.debug("获得了锁 !");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获取锁");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁...");
}
}
}
文章图片
可以看到, 主线程在获得锁后
1s
后释放锁, 而 t1
线程等待2s ,在t1线程释放锁后, 第一时间获取了锁解决哲学家就餐问题 筷子类
需要继承 ReentrantLock 类
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: Chopstick
* @projectName hm-juc-codes
* @description: 筷子类
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 14:37
*/
@Slf4j(topic = "c.Chopstick")
public class Chopstick extends ReentrantLock {private String name;
public Chopstick(String name) {
this.name = name;
}@Override
public String toString() {
return "筷子{" +
"name='" + name + '\'' +
'}';
}
}
哲学家类
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @author 王天赐
* @title: Philosopher
* @projectName hm-juc-codes
* @description: 哲学家类
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 14:37
*/
@Slf4j(topic = "c.Philosopher")
@SuppressWarnings("all")
public 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;
}public void eat() {
log.debug("eat ... ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}@Override
public void run() {
while (true) {
if (left.tryLock()) {
try {
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}} finally {
left.unlock();
}
}
}
}
}
可以看到, 我们需要使用
tryLock
方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后.要在 finally 里释放锁
测试类
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
/**
* @author 王天赐
* @title: TestPhilosopher
* @projectName hm-juc-codes
* @description: 哲学家进餐问题
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 15:24
*/
@SuppressWarnings("all")
@Slf4j(topic = "TestPhilosopher")
public class TestPhilosopher {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, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
公平锁 公平锁一般没有必要,会降低并发度
ReentrantLock 默认是不公平的 , 也就是说并不是按照阻塞队列中先来先得的顺序得到锁的, 随机分配锁的
package cn.knightzz.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestFairLock
* @projectName hm-juc-codes
* @description: 测试公平锁
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 16:31
*/
@SuppressWarnings("all")
public class TestFairLock {public static void main(String[] args) throws InterruptedException {// 构造参数设置公平锁
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0;
i < 500;
i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start...");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入").start();
lock.unlock();
}
}
文章图片
注意:该实验不一定总能复现
如果是公平锁, 随机插入一定是在最后插入
条件变量 基本介绍
synchronized
中也有条件变量,就是waitSet
休息室,当条件不满足时进入waitSet
等待ReentrantLock
的条件变量比synchronized
强大之处在于,它是支持多个条件变量的,这就好比synchronized
是那些不满足条件的线程都在一间休息室等消息,而ReentrantLock
支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
- await 前需要获得锁
- await 执行后,会释放锁,进入
conditionObject
等待 - await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
/**
* @author 王天赐
* @title: TestConditionLock
* @projectName hm-juc-codes
* @description: 测试条件变量
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 16:45
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestConditionLock")
public class TestConditionLock {static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
static boolean hasCigarette = false;
static boolean hasBreakfast = false;
private static void sendCigarette() {
lock.lock();
try {
log.debug("烟送来了 ...");
hasCigarette = true;
// 唤醒对应waitset阻塞的线程
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}}private static void sendBreakfast() {
lock.lock();
try {
log.debug("早餐送来了 ...");
hasBreakfast = true;
// 唤醒对应waitset阻塞的线程
waitBreakfastQueue.signal();
} finally {
lock.unlock();
}
}public static void main(String[] args) throws InterruptedException {new Thread(() -> {
try {
// 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似
lock.lock();
while (!hasCigarette) {
// 不满足条件就到对应的 waitSet 等待
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("等到烟了");
} finally {
lock.unlock();
}
}, "小南").start();
new Thread(() -> {
try {
// 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似
lock.lock();
while (!hasBreakfast) {
// 不满足条件就到对应的 waitSet 等待
try {
waitBreakfastQueue.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("等到早餐了");
} finally {
lock.unlock();
}
}, "小白").start();
sleep(1000);
sendCigarette();
sleep(1000);
sendBreakfast();
}
}
- 小南需要等待烟过来, 否则就一直等待
- 小白需要等待早餐, 否则就一直等待
文章图片
推荐阅读
- spring|Springboot学习笔记
- 【Java基础】String、StringBuffer、StringBuilder
- 【java基础】==和equals
- Java毕业设计项目实战篇|健康菜谱微信小程序+项目前后台源码(JavaSSM+Mysql)
- 面试|史上最详细的一线大厂Mysql面试题详解
- java|互联网公司都怎么实现分页的,拿 MySQL 使劲Limit()
- Spring|XxlJob分布式任务调度平台
- Spring|@Async 没有异步执行
- JAVA随记|Springboot吞吐量优化解决方案