有一段时间没使用过了,复习复习ReentrantLock
相对于 synchronized 它具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与synchronized一样,都可以支持可重入,当然顾名思义,Reentrant 本来就是可重入的意思。
基本语法:
与synchronized不同,synchronized是作为关键字,基于JVM层面对临界区进行保护,而ReentrantLock是基于API的,也就是对象层面。因此需要创建ReenrantLock对象,调用lock()方法来获取锁,使用unlock()方法来释放锁,一定要保证lock() 和 unlock()方法是成对出现的,使用使用 try{}finally{}代码块来保证 unlock()的必须执行,释放锁。
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
try{
// 临界区
}finally {
// 释放锁
reentrantLock.unlock();
}
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被锁住。
可重入代码演示:
/**
* @author Claw
* @date 2022/3/6 11:39.
*/
@Slf4j(topic = "c.ReentrantLock1")
public class ReentrantLock1 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
// 获取锁
reentrantLock.lock();
try {
log.debug("enter main");
m1();
} finally {
// 释放锁
reentrantLock.unlock();
}
}
private static void m1() {
reentrantLock.lock();
try {
log.debug("enter m1");
m2();
} finally {
reentrantLock.unlock();
}
}
private static void m2() {
reentrantLock.lock();
try {
log.debug("entry m2");
} finally {
reentrantLock.unlock();
}
}
}
输出结果:可以看持有同一把锁的方法在重入
可打断
可打断所调用的方法是 lockInterruptibly(),可以尝试获取锁,也能被打断。
下面这段代码演示了 lockInterruptibly()方法的基本使用,由t1线程调用 lockInterruptibly(),来获取锁,这个方法会抛出 InterruptedException 的异常,需要catch住,当抛出这个异常时,说明没有获取到锁,返回。
然后由main线程先获取到锁,再让t1启动,在这一步就会陷入阻塞状态。
用lockInterruptibly() 去打断t1线程,就能避免阻塞,让程序结束。
/**
* @author Claw
* @date 2022/3/6 11:39.
*/
@Slf4j(topic = "c.ReentrantLock2")
public class ReentrantLock2 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// t1 线程尝试获取锁,如果没有获取到则返回,不用一直阻塞.
Thread t1 = new Thread(() -> {
try {
log.debug("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
// 主线程先去获取锁
lock.lock();
// t1 再去执行,就无法拿到锁,进入阻塞状态
t1.start();
// 将T1打断,防止阻塞.
t1.interrupt();
}
}
可打断锁的意义就是加入了可打断的机制,避免了无限制的等待,这也是一种防止死锁的方式。
锁超时
可打断为了避免死等,但是它是一种被动的方式,需要其他线程来调用interrupt()方法。
而锁超时是主动的,它也是一种避免死锁的手段。
立即结束的方式,tryLock() ,代码演示 ()
使用tryLock()方法来获取锁,从名字上也知道,它会尝试去获取锁,返回布尔值来表示是否获取到锁。
使用while来判断,当获取不到锁时,直接return,避免阻塞。
Main线程先获取锁,再让t1启动
/**
* @author Claw
* @date 2022/3/6 11:39.
*/
@Slf4j(topic = "c.ReentrantLock3")
public class ReentrantLock3 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 尝试获取锁,没有获取到则返回
if (!lock.tryLock()) {
log.debug("{}没有获取到锁,返回", Thread.currentThread().getName());
return;
}
try {
log.debug("{}获取到锁", Thread.currentThread().getName());
} finally {
lock.unlock();
}
}, "t1");
// 主线程先获取锁
lock.lock();
log.debug("{}获取到锁", Thread.currentThread().getName());
// t1 此时执行,没有获取到锁.
t1.start();
}
}
结果:
等待一段时间结束的方式,tryLock(long timeout, TimeUnit unit),代码演示
tryLock()第二种使用方式,带时间参数的方法。在指定时间没有获取到锁的时候,会结束等待。
主线程先拿到锁,然后启动t1,此时开始等待2秒,1秒以后main线程进入睡眠,调用unlock()方法解锁,t1在超时时间内尝试获取锁成功。
/**
* @author Claw
* @date 2022/3/6 11:39.
*/
@Slf4j(topic = "c.ReentrantLock4")
public class ReentrantLock4 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 尝试获取锁,没有获取到则返回
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("{}没有获取到锁,返回", Thread.currentThread().getName());
return;
}
} catch (InterruptedException e) {
// tryLock 也可以被打断,会抛出 InterruptedException
e.printStackTrace();
log.debug("{}没有获取到锁,返回", Thread.currentThread().getName());
return;
}
try {
log.debug("{}获取到锁", Thread.currentThread().getName());
} finally {
lock.unlock();
}
}, "t1");
// 主线程先获取锁
lock.lock();
log.debug("{}获取到锁", Thread.currentThread().getName());
// t1 此时执行,没有获取到锁.
t1.start();
try {
Thread.sleep(1000);
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
公平锁
ReentrantLock 默认是非公平锁,synchronized也是非公平锁。
当多个线程竞争一把锁的时候,谁先得到谁就能拿到锁,并不是先来后到的顺序,谁先等待谁先获得,这种称为非公平锁。
但是ReentrantLock 可以通过构造方法来设置称为公平锁。
ReentrantLock lock = new ReentrantLock(true);
构造方式源码,可以看到当设置为true的时候,返回一个公平锁,调整为公平锁以后,线程获得锁的方式就变为了哪个线程先进入等待队列, 谁就先拿到锁。
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁的本意是解决线程饥饿的问题,有均等的机会通过先入先得的方式拿到锁,但是实际上,公平锁一般没有必要,会降低并发度。
条件变量
synchronized 也有条件变量,在synchronized原理中,提到过Monitor里的waitSet,当线程拿不到锁的时候,会进入waitSet里面去等待。
ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的。
synchronized 把不能获取锁的 阻塞线程都放入了waitSet里等待,notifyAll()会叫醒所有的线程,一些线程并不能满足拿到锁的条件,造成了虚假唤醒。
ReentrantLock的条件变量更灵活,condition相当于多个条件,当唤醒线程时,唤醒可以满足条件的线程,这就更为灵活。
使用流程:
- await 前需要获得锁
- await 执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(打断、超时)获取重新竞争lock锁
- 竞争lock锁成功后,从await后继续执行
代码演示:
代码思路,用两个线程来模拟工作的人,一个需要music才能工作,一个要有coffee才能工作,当条件不满足时,它们都进入了等待。
此时再安排两个线程,一个送咖啡,一个塞耳机,把hasMusic和hasCoffee设为true,并唤醒等待music和coffee的线程。
/**
* @author Claw
* @date 2022/3/6 11:39.
*/
@Slf4j(topic = "c.ReentrantLock5")
public class ReentrantLock5 {
static ReentrantLock lock = new ReentrantLock(false);
static Condition musicCondition = lock.newCondition();
static Condition coffeeCondition = lock.newCondition();
static boolean hasMusic;
static boolean hasCoffee;
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
log.debug("有music吗?{}", hasMusic);
while (!hasMusic) {
log.debug("没有music,不想工作");
musicCondition.await();
}
log.debug("有music了,claw开始干活");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "一个需要music的claw").start();
new Thread(() -> {
lock.lock();
try {
log.debug("有coffee吗?{}", hasMusic);
while (!hasCoffee) {
log.debug("没有coffee,不想工作");
coffeeCondition.await();
}
log.debug("有coffee了,PuppyCoding开始干活");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "一个需要coffee的PuppyCoding").start();
new Thread(() -> {
lock.lock();
try {
hasCoffee = true;
coffeeCondition.signalAll();
log.debug("送了coffee过来");
} finally {
lock.unlock();
}
}, "送coffee").start();
new Thread(() -> {
lock.lock();
try {
hasMusic = true;
musicCondition.signalAll();
log.debug("塞上耳机,music!");
} finally {
lock.unlock();
}
}, "有music").start();
}
}
结果展示:
相较于synchronized 的notify 和notifyAll 叫醒的线程,使用ReentrantLock的条件唤醒的线程更精确。
注意事项
- lock和unlock必须要成对使用,unlock必须要在finally中释放
- 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。
总结
根据ReentrantLock的特点:可重入,可超时/打断,支持多个的条件condition,还能设置为公平锁,可以整理出与synchronized异同。
比synchronized好的地方:
- 可以设置超时时间,也可以打断,不用一直阻塞等待
- 可以设置为公平锁
- condition 让线程之间的通信更加灵活
- ReentrantLock让同步代码块更加灵活,比如使用synchronized的时候只能在一个结果中获取和释放
ReentrantLock底层原理
要说ReentrantLock的原理就要先说AQS。
AQS是用来构建锁和其他同步器组件的重量级基础框架和基石,通过内置的队列来完成资源获取线程的排队工作,并通过一个int类型的变量来表示持有锁的状态。
ReentrantLock 非公平锁锁实现原理(课程笔记,待完善)
先从构造器看,默认为非公平锁实现
NonfairSync继承sync,sync又继承于AbstractQueuedSynchronizer
没有竞争时,用compareAndSetState去修改state,如果成功了修改为1,并且把exclusiveOwnerTheard设置为自己,这样就加锁成功了。
如果出现了竞争时
如果出现了竞争,在刚刚上面的源码中会走到else逻辑,即调用acquire方法,它会再去尝试一次能不能获取到锁,没有获取到的话就走acquireQuened(addWaiter)的逻辑,构造Node队列。
在没有竞争的情况下,一开始会造一个空节点来进行占位,Node节点是一个双向的链表,此时Thread1竞争失败,排在空节点后面,再进入acuireQueued的逻辑。
- acuireQueued会在一个死循环中不断尝试获得锁,然后进入Park阻塞。
- 如果主机紧邻着head(排第二位),那么在此tryAcquire尝试获取锁,当然这时state仍为1,失败。
- 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1,这次返回flase(修改为-1的时候表示前驱node有责任在锁释放的时候唤醒后驱node)