目录
一、ReentrantLock的含义
二、RerntrantLock当中的常用方法
①lock()和unlock()方法
②构造方法
③tryLock()方法
tryLock()无参数
tryLock(timeout,Times)有参数
④lockInterruptibly() throws InterruotedException
经典面试问题:
ReentrantLock和synchronized有什么不同
使用方法上面的区别
①ReentrantLock可以提供公平+非公平两种特性
②ReentrantLock的加锁、解锁操作都是需要手动进行,
③synchronized无法提供lock.tryLock()这样的尝试获取锁的特性,而ReentrantLock可以提供。
④ReentrantLock可以提供中断式加锁。
⑤ReentrantLock借助Condition类:可以指定唤醒线程
锁的实现方式上面的区别
提供的级别不一样:
原理不一样:
ReentrantLock的原理
一、ReentrantLock的含义
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的各个特性。
二、RerntrantLock当中的常用方法
①lock()和unlock()方法
顾名思义,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()方法
tryLock()无参数
tryLock()方法有两个作用:
当调用lock.tryLock()的时候,如果lock此时还没有被其他线程占有,那么它会立刻获取到锁,并且返回true。
如果lock已经被其他线程占用了,那么调用lock.tryLock()的线程将不会阻塞等待,而是继续往下执行。
tryLock(timeout,Times)有参数
当线程调用lock.tryLock(timeout,TimeUtil.时间单位常量)
方法的时候,会发生以下的情况:
(1)当前线程将会在lock.tryLock(timeout,TimeUtil.时间单位常量)这行代码处阻塞等待timeout时间,如果获取到锁的线程在这个timeout时间内释放锁了,那么正在等待的线程可以重新获取锁。
(2)如果阻塞等待的线程直到timeout时间了,加锁的线程仍然没有释放锁,那么原来在等待的线程将不再等待,直接返回。
(3)如果超时等待的线程在等待锁释放的timeout时间内被中断(其他线程调用t.interrupt())方法中断正在等待的线程,那么当前正在等待的线程会抛出InterruptException,也会终止等待
因此,正确使用tryLock()的方式为:首先进行判断,如果得到结果为false,也就是获取不到锁,直接return返回即可。
④lockInterruptibly() throws InterruotedException
这个方法,类似于"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();//标志位默认为falsewhile (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() {@Overridepublic void run() {count1.add();}},"t1");t1.start();//让主线程休眠1000毫秒,确保t1一定启动成功了try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}Thread t2=new Thread(new Runnable() {@Overridepublic void run() {count1.add();}},"t2");t2.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//中断t2t2.interrupt();}
}
图解:
但是,一运行程序,发现出现了下面的问题:
出现第一个问题的原因我们找到了,那出现第二个问题,,锁状态异常又是什么原因呢?
回到add()方法当中:
这就是为什么thread2在被唤醒之后,触发锁状态异常的原因。
经典面试问题:
ReentrantLock和synchronized有什么不同
使用方法上面的区别
①ReentrantLock可以提供公平+非公平两种特性
当ReentrantLock构造方法中指定了参数为true的时候,这个锁被确定为公平锁。
而synchronized无法提供公平锁的特性
②ReentrantLock的加锁、解锁操作都是需要手动进行,
而synchronized的话可以进行自动的加锁、解锁操作。
synchronized可以有效避免加锁之后忘记解锁的情况。
当代码执行到synchronized修饰的代码块的时候,如果在同步代码块内部发生了异常,没有及时处理的话,会提前退出并且让线程释放锁。
而ReentrantLock无法做到立刻解锁,因此,unLock()的解锁操作一定要在finally代码块当中,避免加锁之后忘记解锁的情况。
③synchronized无法提供lock.tryLock()这样的尝试获取锁的特性,而ReentrantLock可以提供。
线程如果在指定的时间之内无法获取到锁,或者锁已经被占用了,那么lock.tryLock()可以有效减少线程阻塞等待的情况,或者减少阻塞等待的时间。
而synchronized只会让无法获取到锁的线程"死等"。直到获取到锁的线程释放锁
④ReentrantLock可以提供中断式加锁。
④ReentrantLock在调用lock.lockInterruptibly()时候,可以让获取不到锁,进入阻塞等待的线程被提前"唤醒",但是synchronized不可以。具体的操作已经在上面解释了。
下面,给一个场景,验证可中断式加锁。
public static void main(String[] args) throws InterruptedException {ReentrantLock lock=new ReentrantLock(true);Thread thread=new Thread(new Runnable() {@Overridepublic void run() {//让thread获取到锁lock.lock();System.out.println("thread获取到了锁");//不提供解锁的操作,一直让thread占有这把锁}});thread.start();//确保thread已经启动,并且持有锁了Thread.sleep(1000);Thread thread1=new Thread(new Runnable() {@Overridepublic void run() {//不让thread1获取到锁,同时thread1是"可中断式"加锁try {lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();System.out.println("thread1的阻塞等待被中断了");}}});thread1.start();//main线程尝试唤醒thread1thread1.interrupt();}
运行程序,可以看到:thread1的阻塞被中断了
⑤ReentrantLock借助Condition类:可以指定唤醒线程
指定了可以notify()的线程。
类比于synchronized,如果多个线程因为获取不到锁进入了WAITING或者TIME_WAITING状态。那么,在notify()的时候,只可以随机唤醒一个正在WAITING或者TIME_WAITING的线程。
但是ReentrantLock借助Condition接口,可以指定唤醒线程。
锁的实现方式上面的区别
提供的级别不一样:
ReentrantLock是Java当中的一个具体的类,是在API级别提供的锁,
而synchronized是Java当中提供的一个关键字,是JVM级别提供的锁
原理不一样:
synchronized加锁的过程,涉及了锁升级的过程。
从无锁->偏向锁->轻量级锁->重量级锁;并且还可能涉及锁粗化、锁消除。
在下面这一篇文章当中已经提到了:
(2条消息) 【JavaEE多线程】synchronized原理篇_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128826633?spm=1001.2014.3001.5502 但是,ReentrantLock是基于AQS来实现的。
在这一篇文章当中,我们也提到了什么是AQS,它的核心就是两个属性。一个是内部封装的队列。用来保存获取不到锁的线程。另外一个是state属性,用来标记是否可以获取锁。
(2条消息) Java当中的AQS_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128664083?spm=1001.2014.3001.5502
ReentrantLock的原理
ReentrantLock在初始化的时候,就提供了公平和非公平的两种方式。
在ReentrantLock内部封装了两个静态内部类,一个是FairSync,另外一个是NofairSymc。
分别提供了公平的加锁方式和不公平的加锁方式。
这两个类都继承于ReentrantLock内部的一个Syn,然后这个Syn又继承于AQS。
当有线程调用lock方法的时候:
如果线程获取到锁了,那么就会通过CAS的方式把AQS内部的state设置成为1。
这个时候,当前线程就获取到锁了。
可以看到,只有首部的节点(head节点封装的线程)可以获取到锁。
其他线程都会加入到这一个阻塞队列当中。
如果是公平锁的话,当head节点释放锁之后,会优先唤醒head.next这一个节点对应的线程。令head=head.nxet,让下一个节点对应的线程获取到锁。
如果是非公平锁的话,会让之前head节点之后的节点对应的线程一起采用CAS的方式获取锁。