目录
一、ReentrantLock的含义
二、RerntrantLock当中的常用方法
①lock()和unlock()方法
②构造方法
③tryLock()方法
tryLock()无参数
tryLock(timeout,Times)有参数
④lockInterruptibly() throws InterruotedException
经典面试问题:
ReentrantLock和synchronized有什么不同
ReentrantLock也是Java当中提供的一种锁。这种锁和synchronized类似也可以起到互斥使用,保证线程安全的的作用。
关于synchronized的作用,已经在这一篇文章当中提及:
(3条消息) Java对于synchronized的初步认识_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128062475?spm=1001.2014.3001.5501
但是,仍然在使用的语法上面和synchronized有一些差别。下面,将具体介绍一下ReentrantLock的使用以及ReentrantLock的各个特性。
顾名思义,lock()方法就是线程进入同步代码块之后的加锁操作,而unlock()就是需要离开代码块之后的解锁操作。
在上述代码当中,锁就是lock对象。当多个线程同时尝试调用lock.lock()方法之后,只有其中一个线程可以获得锁,其余线程都需要阻塞等待。当线程执行到unlock()方法之后,说明已经解锁了,其他线程可以继续获取lock。
但是, 上面的写法不是规范的写方法,规范的写法,应当把lock.unlock()写进finally代码块当中,并且lock.lock()方法需要在try方法上方的第一行
’ 如下图所示:
原因:
如果在上述代码当中,出现了if语句,线程进入if语句之后调用了lock.lock()方法加锁成功。但是线程离开if语句的时候,没有调用lock.unlock()方法,这样也就意味着线程提前返回了,没有解锁。那么其他线程如果有阻塞等待的,将一直阻塞等待。
因此,为了防止忘记解锁的情况,应当把lock.unlock()放到finally当中。
如图:故意不解锁,看看有什么后果。场景,此时有两个线程,一个是thread1,另外一个是thread2。两个线程分别通过同一个对象count调用add()方法:
可以看到,控制台始终输出了-->"现在的线程是....thread1...",说明thread2无法再次获取到锁。
ReentrantLock lock1=new ReentrantLock(true);
ReentrantLock lock=new ReentrantLock(false);
如果构造方法当中,指定了true作为参数,那么lock将是公平锁。如果没有指定布尔值,或者指定了布尔值为false,那么lock将是非公平锁。
tryLock()方法有两个作用:
当调用lock.tryLock()的时候,如果lock此时还没有被其他线程占有,那么它会立刻获取到锁,并且返回true。
如果lock已经被其他线程占用了,那么调用lock.tryLock()的线程将不会阻塞等待,而是继续往下执行。
当线程调用lock.tryLock(timeout,TimeUtil.时间单位常量)
方法的时候,会发生以下的情况:
(1)当前线程将会在lock.tryLock(timeout,TimeUtil.时间单位常量)这行代码处阻塞等待timeout时间,如果获取到锁的线程在这个timeout时间内释放锁了,那么正在等待的线程可以重新获取锁。
(2)如果阻塞等待的线程直到timeout时间了,加锁的线程仍然没有释放锁,那么原来在等待的线程将不再等待,直接返回。
(3)如果超时等待的线程在等待锁释放的timeout时间内被中断(其他线程调用t.interrupt())方法中断正在等待的线程,那么当前正在等待的线程会抛出InterruptException,也会终止等待
因此,正确使用tryLock()的方式为:首先进行判断,如果得到结果为false,也就是获取不到锁,直接return返回即可。
这个方法,类似于"lock"也是属于"加锁"的方法。
和单纯的lock()方法不同,线程调用了lockInterrupt()方法之后,可以"响应中断"式地加锁。
假设,在某一时刻,t1线程获取到锁,在t1调用lockInterruptibly()方法获取到锁之后,如果t2也调用这个方法获取锁,那么t2会进入阻塞等待的状态。
如果t2在阻塞等待的过程当中,被其他线程调用t2.interrupt()方法,那么线程t2会被触发异常(InterruptException),并且被"唤醒"。
代码实现:
add()方法,使用sleep(1000)的目的是减慢循环的速度
class Count1{
public int number;
ReentrantLock lock=new ReentrantLock();
public void add(){
try {
//使用"可中断"式地加锁
lock.lockInterruptibly();
//标志位默认为false
while (true){
number++;
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
启动t1,t2线程,让t1先获取到锁,t2后面才获取到锁:
public class ThreadDemo32 {
public static void main(String[] args) {
Count1 count1=new Count1();
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
count1.add();
}
},"t1");
t1.start();
//让主线程休眠1000毫秒,确保t1一定启动成功了
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
count1.add();
}
},"t2");
t2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t2
t2.interrupt();
}
}
图解:
但是,一运行程序,发现出现了下面的问题:
出现第一个问题的原因我们找到了,那出现第二个问题,,锁状态异常又是什么原因呢?
回到add()方法当中:
这就是为什么thread2在被唤醒之后,触发锁状态异常的原因。
①ReentrantLock可以提供公平+非公平两种特性,当构造方法中指定了参数为true的时候,这个锁被确定为公平锁。
而synchronized无法提供公平锁的特性
②ReentrantLock的加锁、解锁操作都是需要手动进行,而synchronized的话可以进行自动的加锁、解锁操作。
synchronized可以有效避免加锁之后忘记解锁的情况。
③synchronized无法提供lock.tryLock()这样的特性,而ReentrantLock可以提供。线程如果在指定的时间之内无法获取到锁,或者锁已经被占用了,那么lock.tryLock()可以有效减少线程阻塞等待的情况,或者减少阻塞等待的时间,
而synchronized只会让无法获取到锁的线程"死等"。直到获取到锁的线程释放锁
④ReentrantLock在调用lock.lockInterruptibly()时候,可以让阻塞等待的线程被提前"唤醒",但是synchronized不可以。具体的操作已经在上面解释了。
⑤ReentrantLock是Java当中的一个具体的类,是在API级别提供的锁,
而synchronized是Java当中提供的一个关键字,是JVM级别提供的锁
⑥当代码执行到synchronized修饰的代码块的时候,如果在同步代码块内部发生了异常,没有及时处理的话,会提前退出并且让线程释放锁。
而ReentrantLock无法做到立刻解锁,因此,unLock()的解锁操作一定要在finally代码块当中,避免加锁之后忘记解锁的情况。