【多线程:ReentrantLock】
01.介绍
RennttrantLock也是锁和synchronized一样具有锁定同步代码块的作用,不过和synchronized还是有很多不一样的地方。
RennttrantLock是java.util.concurrent(juc) 下的一个类,相对于synchronized它具备如下特点:
1.可以打断处于BLOCKED状态的其他线程
2.可以设置处于BLOCKED状态的线程超时时间,如果超过这个时间就放弃争抢锁
3.可以设置为公平锁,线程按照先入先出获取锁
4.支持多个条件变量,这个是指 对于等待区 我们可以由多个 不同对象调用锁处于等待状态时所在的等待区不同,对于synchronized锁来说 它的等待区就只有一个WaitSet
与synchronized一样 都支持锁重入,即自己可以再次获取自己内部的锁
02.基本语法
ReentrantLock lock = new ReentrantLock();
lock.lock();
try{
// lock.lock();
// 临界区
}finally{
// 释放锁
lock.unlock();
}
注意:lock.lock();可以放在try外面,也可以放在try里面,最后在finally中需要进行lock.unlock();释放锁
03.可锁重入
例子
public class TestInterKCR {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("enter main");
m1();
}finally {
lock.unlock();
}
}
private static void m1(){
lock.lock();
try {
System.out.println("enter m1");
m2();
}finally {
lock.unlock();
}
}
private static void m2(){
lock.lock();
try {
System.out.println("enter m2");
}finally {
lock.unlock();
}
}
}
结果
enter main
enter m1
enter m2
解释
可以看出自己内部的锁都可以调用,没有发生死锁,说明可以锁重入
04.可打断
介绍
我们先要明确一个概念,这里说的可打断 说的是 一个已经获得锁的线程t1 和一个处于阻塞状态线程的t2(因为t1已经获取了锁),我们可以在其他线程中 把 t2线程打断 使之 跳出阻塞状态 运行其它部分代码。
synchronized为什么不可以打断
例子
public class TestInterSync {
private static String a = "s";
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println("t1尝试获取锁");
synchronized (a){
System.out.println("t1获取到锁");
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted){
System.out.println("t1被打断");
}
}
});
Thread t2 = new Thread(()->{
System.out.println("t2尝试获取锁");
synchronized (a){
System.out.println("t2获取到锁");
while (true){
}
}
});
t2.start();
Sleeper.sleep(0.5);
t1.start();
Sleeper.sleep(0.5);
System.out.println("打断正在等待锁的t1线程");
t1.interrupt();
}
}
结果
程序没有结束:
t2尝试获取锁
t2获取到锁
t1尝试获取锁
打断正在等待锁的t1线程
解释
我们发现程序并没有结束,我们的t2线程先获取到锁且没有释放锁,导致t1线程阻塞,现在我们在主线程中打断t1线程 但是发现无法打断
lock方法可以打断吗?
例子
public class TestInterReenY {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println("t1尝试获取锁");
lock.lock();
try {
System.out.println("t1获取到锁");
}finally {
lock.unlock();
}
});
Thread t2 = new Thread(()->{
System.out.println("t2尝试获取锁");
lock.lock();
try {
System.out.println("t2获取到锁");
Sleeper.sleep(2);
}finally {
}
});
t2.start();
Sleeper.sleep(0.5);
t1.start();
Sleeper.sleep(0.5);
System.out.println("打断正在等待锁的t1线程");
t1.interrupt();
}
}
结果
程序没有结束:
t2尝试获取锁
t2获取到锁
t1尝试获取锁
打断正在等待锁的t1线程
解释
我们发现lock方法依然不能打断处于阻塞状态的线程,那我们应该用什么,答案是lockInterruptibly方法
lockInterruptibly方法
例子
public class TestInterReen {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {
System.out.println("t1尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("t1被打断");
return;
}
try {
System.out.println("t1获取到锁");
}finally {
lock.unlock();
}
});
Thread t2 = new Thread(()->{
try {
System.out.println("t2尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("t2被打断");
return;
}
try {
System.out.println("t2获取到锁");
Sleeper.sleep(2);
}finally {
}
});
t2.start();
Sleeper.sleep(0.5);
t1.start();
Sleeper.sleep(0.5);
System.out.println("打断正在等待锁的t1线程");
t1.interrupt();
}
}
结果
程序结束:
t2尝试获取锁
t2获取到锁
t1尝试获取锁
打断正在等待锁的t1线程
t1被打断
解释
可以看出我们用lockInterruptibly方法可以实现 打断处于阻塞状态的线程
05.锁超时
介绍
锁超时是指,一旦锁被其他线程占用 如果没有指定超时时间 则本线程会直接跳出阻塞状态 不参与竞争,如果指定了超时时间 则 超时时间内处于阻塞状态 超出超时时间 则跳出阻塞状态。
例子:无时限
@Slf4j(topic = "c.TestInterCS")
public class TestInterCS {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("t1尝试获取锁");
if (!lock.tryLock()){
log.debug("t1获取不到锁");
return;
}
try {
log.debug("t1获取到锁");
}finally {
}
},"t1");
Thread t2 = new Thread(()->{
log.debug("t2尝试获取锁");
if (!lock.tryLock()){
log.debug("t2获取不到锁");
return;
}
try {
log.debug("获取到锁");
}finally {
lock.unlock();
}
},"t2");
t1.start();
Sleeper.sleep(1);
t2.start();
}
}
结果
00:40:27.595 c.TestInterCS [t1] - t1尝试获取锁
00:40:27.597 c.TestInterCS [t1] - t1获取到锁
00:40:28.600 c.TestInterCS [t2] - t2尝试获取锁
00:40:28.600 c.TestInterCS [t2] - t2获取不到锁
解释
可以看出t1线程获取到锁并且没有释放,t2线程一直处于阻塞状态,我们用tryLock方法 且没有指定超时时间 也就是一旦发现t2线程处于阻塞状态lock.tryLock()返回false,我们执行其它代码
例子:有时限
@Slf4j(topic = "c.TestInterCS")
public class TestInterCS {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("t1尝试获取锁");
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)){
log.debug("t1获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("t1获取到锁");
}finally {
}
},"t1");
Thread t2 = new Thread(()->{
log.debug("t2尝试获取锁");
if (!lock.tryLock()){
log.debug("t2获取不到锁");
return;
}
try {
log.debug("获取到锁");
}finally {
lock.unlock();
}
},"t2");
t1.start();
Sleeper.sleep(1);
t2.start();
}
}
结果
00:44:46.180 c.TestInterCS [t1] - t1尝试获取锁
00:44:46.183 c.TestInterCS [t1] - t1获取到锁
00:44:47.183 c.TestInterCS [t2] - t2尝试获取锁
00:44:47.183 c.TestInterCS [t2] - t2获取不到锁
解释
我们给tryLock方法指定时间为2s,但是因为t1线程始终没有释放锁,所以结果lock.tryLock()依然返回false,我们进行逻辑判断,执行其它的代码
哲学家就餐问题
介绍
之前我们介绍过哲学家就餐问题,当时我们用 顺序加锁的方法解决了死锁的问题,但是当时产生了饥饿问题,这次我们用锁超时解决饥饿问题
代码
@Slf4j(topic = "c.Test23")
public class Test23 {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();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if(left.tryLock()) {
try {
// 尝试获得右手筷子
if(right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock(); // 释放自己手里的筷子
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
结果
.
.
.00:54:18.866 c.Philosopher [苏格拉底] - eating...
00:54:18.866 c.Philosopher [亚里士多德] - eating...
00:54:19.373 c.Philosopher [柏拉图] - eating...
00:54:19.373 c.Philosopher [阿基米德] - eating...
00:54:19.881 c.Philosopher [苏格拉底] - eating...
00:54:19.881 c.Philosopher [赫拉克利特] - eating...
00:54:20.386 c.Philosopher [苏格拉底] - eating...
00:54:20.386 c.Philosopher [亚里士多德] - eating...
00:54:20.894 c.Philosopher [亚里士多德] - eating...
00:54:20.894 c.Philosopher [苏格拉底] - eating...
00:54:21.401 c.Philosopher [赫拉克利特] - eating...
00:54:21.401 c.Philosopher [柏拉图] - eating...
00:54:21.908 c.Philosopher [苏格拉底] - eating...
00:54:21.908 c.Philosopher [赫拉克利特] - eating...
00:54:22.417 c.Philosopher [苏格拉底] - eating...
00:54:22.417 c.Philosopher [赫拉克利特] - eating...
.
.
.
.
解释
我们可以看出现在没有产生死锁 且 也没有线程处于饥饿,这里的做法是让Chopstick继承ReentrantLock类,然后Chopstick对象也就是筷子 使用tryLock方法 如果和其他哲学家产生竞争就放弃获取,这样自然而然没有了 由于互相争抢而产生的死锁问题了
06.公平锁
介绍
多个线程按照申请锁的顺序获取锁
源码
可以看出,ReentrantLock有参构造传递的为true则创建出的对象就为公平锁(默认是不公平锁),公平锁一般没有必要 会降低并发度,之后我们学习AQS时会分析它的源码
07.条件变量
介绍
synchronized中也有条件变量,就是我们讲原理时Monitor中的WaitSet,当条件不满足时进入WaitSet中等待。
ReentrantLock的条件变量比synchronized强大之处在于 它是之处多个条件变量的 这就好比 synchronized是那些不满足条件的线程都在一间休息室等消息,而ReentrantLock支持多间休息室 有专门等烟条件的休息室 有专门等待早餐条件的休息室 唤醒时也是按照休息室来唤醒
使用流程
1.await前需要获取锁
2.await执行后 会释放锁 进入conditionObject等待
3.await的线程被唤醒(或打断 或超时)去重新竞争lock锁
4.竞争lock锁成功后 从await后继续执行
例子
还是之前那个wait/notify的例子,这次使用RenntrantLock实现
@Slf4j(topic = "c.Test24")
public class Test24 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
结果
01:20:49.292 c.Test24 [小南] - 有烟没?[false]
01:20:49.295 c.Test24 [小南] - 没烟,先歇会!
01:20:49.295 c.Test24 [小女] - 外卖送到没?[false]
01:20:49.295 c.Test24 [小女] - 没外卖,先歇会!
01:20:50.300 c.Test24 [小女] - 可以开始干活了
01:20:51.309 c.Test24 [小南] - 可以开始干活了
解释
我们创建了两个休息室,小南在没有获得烟这个条件前 进入的是waitCigaretteSet休息室,小女在没有获得外卖这个条件前 进入的是waitTakeoutSet这个休息室,相比于之前使用synchronized时小女和小南没有获得条件之前进入的都是WaitSet,这里更加细分了。后面唤醒时 也是按照休息区来唤醒。