你可能觉得很陌生,那我就把它和synchronized比较一下!
与synchronized区别:
不同点:
-
可中断,运行代码块时,可以中断。
-
可以设置超时时间,例如:等待一段时间之后,还没有获取到锁,那么就放弃等待,去执行其他的任务代码。
-
可以设置为公平锁,可以防止线程饥饿。
-
支持多个条件变量,相当于有多个WaitSet等待队列,根据等待条件的不同而区分。
相同点:
-
都支持可重入
基本用法:
//创建一个ReentrantLock对象
ReentrantLock reentrantLock= new ReentrantLock();
//代码块
reentrantLock.lock();
try{
//临界区
}finally{
reentrantLock.unlock();
}
1、ReentrantLock的锁重入
概念:如果一个线程首次获得了这把锁,那么它就是该锁的owner,因此可以再次获取这把锁。
(如果不允许锁重入,那么第二次获得锁时,自己也会被锁住。)
代码演示:
@Slf4j
public class ReentrantLockTest {
private static ReentrantLock reentrantLock= new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock();
try{
//临界区
log.debug("主线程lock临界区");
//主线程获取锁之后,成为ReentrantLock锁的owner,同时调用方法1,再次获取ReentrantLock锁
menthod1();
}finally {
reentrantLock.unlock();
}
}
private static void menthod1() {
reentrantLock.lock();
try{
log.debug("m1的lock临界区");
//方法1中调用方法2,方法2再次获取ReentrantLock锁
menthod2();
}finally {
reentrantLock.unlock();
}
}
private static void menthod2() {
reentrantLock.lock();
try{
log.debug("m2的lock临界区");
}finally {
reentrantLock.unlock();
}
}
}
测试结果:可以全部输出,因此ReentrantLock可以锁重入。
2、ReentrantLock的可打断
概念:当线程A获取锁时有竞争,线程A就进入阻塞队列,但是可以被其它线程用interrupt()方法打断。
看一个例子:
@Slf4j
public class ReentrantLockTest1 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t = new Thread(()->{
//调用可打断的方法
try {
log.debug("尝试获取锁");
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获取到锁");
return;
}
try{
log.debug("获取到锁");
}finally {
reentrantLock.unlock();
}
});
//主线程占用锁
reentrantLock.lock();
t.start();
//打断t线程的等待
t.interrupt();
}
}
测试结果如下:
在线程t调用start()方法之前,主线程已经获取了reentrantLock锁,那么线程t只能等待,但是主线程调用了t线程的interrupt()方法,打断了t线程的等待,此时t线程没有获取到锁,并且抛出了异常。这种方式不让线程无限制的等待下去,一定程度的避免了死锁。
3、锁超时
概念:线程获取锁时,设置一个时间限制,如果在时间限制之内获得了锁;则获取成功,如果获取超时,则获取失败。
基本用法:
//创建锁
ReentrantLock reentrantLock = new ReentrantLock();、
//获取锁
try{
//方法一:立刻获取锁,获取不到直接返回
if(!reentrantLock.tryLock()){}
//方法二:在等待的时间范围内获取锁,等待超时就返回
if(!reentrantLock.tryLock(时间,时间单位)){}
}catch (InterruptedException e) {
e.printStackTrace();
return;//没有获取到锁就直接返回,不执行后面的代码
}
try{
//获取到锁才会释放锁
}finally{
//释放锁
reentrantLock.unlock();
}
实例演示:
@Slf4j
public class ReentrantLockTest2 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
log.debug("获取锁");
try {
//使用方法二:在设置的等待时间范围内获取锁
if (!reentrantLock.tryLock(2, TimeUnit.SECONDS)){
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
//超过等待时限,也没有获取到锁,也需要返回
return;
}
try{
log.debug("获取到了锁");
}finally {
reentrantLock.unlock();
}
});
//主线程获取锁
reentrantLock.lock();
t.start();
sleep(1000);
//主线程释放了锁
reentrantLock.unlock();
}
}
测试结果:
可以看到,t线程获取锁等待时间是2秒,主线程在获取锁之后等待一秒就释放了,所以t线程可以获取到锁。
利用锁超时解决哲学家就餐问题
@Slf4j
public class Thread1 {
public static void main(String[] args) throws InterruptedException {
//筷子对象
Chopsticks c1 = new Chopsticks("c1");
Chopsticks c2 = new Chopsticks("c2");
Chopsticks c3 = new Chopsticks("c3");
Chopsticks c4 = new Chopsticks("c4");
Chopsticks c5 = new Chopsticks("c5");
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();
}
}
//筷子
class Chopsticks extends ReentrantLock {
private String name;
public Chopsticks(String name) {
this.name = name;
}
}
//哲学家
@Slf4j
class Philosopher extends Thread{
//名字
private String name;
//筷子
private Chopsticks left;
private Chopsticks right;
public Philosopher(String name, Chopsticks left, Chopsticks right) {
super(name);
this.name = name;
this.left = left;
this.right = right;
}
@SneakyThrows
@Override
public void run() {
while(true){
//获取左手的筷子
if (left.tryLock()){
try{
//获取右手的筷子
if (right.tryLock()){
try{
eat(name);
}finally {
right.unlock();
}
}
}finally {
//吃饭饭放下筷子,或者只拿到左手的筷子而没拿到右手的筷子都要释放锁
left.unlock();
}
}
}
}
private void eat(String name) throws InterruptedException {
log.debug(name + "正在吃饭");
sleep(1000);
}
}
这样就不会造成死锁,同时也避免了线程饥饿问题。
测试结果:
4、条件变量
概念:使用ReentrantLock时,当线程获取到锁之后,执行同步代码块时有条件不成立,需要等待时,就会进入到像synchronized的WaitSet的地方等待,但是ReentrantLock会更加强大,因为它的条件变量会更多,让线程在多个地方等待,唤醒线程时,也是按照等待的地方来唤醒的。
基本使用:
//创建锁
private static ReentrantLock reentrantLock = new ReentrantLock();
//创建条件变量
Condition c = reentrantLock.newCondition();
//要先加锁
reentrantLock.lock();
//调用Condition的await()方法 等待
c.await();
//唤醒
c.signal();//唤醒一个
c.signalAll();//唤醒条件变量c中的全部
注意事项:
-
await()之前需要获得锁
-
await()执行之后会释放锁
-
await()等待的线程被唤醒、或者打断、或者超时,去重新竞争锁。
-
竞争ReentrantLock成功之后,从await()方法后继续执行。