什么是可重入锁?
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
可重入锁有
- synchronized
- ReentrantLock
优点:可一定程度避免死锁。
1>.
可重入锁(递归锁)
-
①. 指的是同一线程外层函数获得锁后,再进入该线程的内层方法会自动获取锁 (
前提,锁对象是同一个对象
)
类似于家里面的大门,进入之后可以进入厕所、厨房等 -
②. Java中ReentranLock(显示锁)和synchronized(隐式锁)都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
-
③.
隐式锁:
(即synchronized关键字使用的锁)默认是可重入锁(同步块、同步方法)
原理如下:掌握
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,否则需要等待,直至持有线程释放该锁
- 当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放
在这里插入图片描述
javap -c ***.class
防止异常要保证彻底释放锁和退出。
//1.同步块
public class SychronizedDemo {
Object object=new Object();
public void sychronizedMethod(){
new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t"+"外层....");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t"+"中层....");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t"+"内层....");
}
}
}
},"A").start();
}
public static void main(String[] args) {
new SychronizedDemo().sychronizedMethod();
/*
输出结果:
A 外层....
A 中层....
A 内层....
* */
}
}
public class Test {
public static void main(String[] args) {
final Lock lock = new ReentrantLock();
new Thread(()->{
lock.lock();
lock.lock();
try {
System.out.println("============外层");
lock.lock();
try {
System.out.println("============内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
// lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
lock.lock();
try {
System.out.println("============外层");
lock.lock();
try {
System.out.println("============内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
lock.unlock();
}
},"t1").start();
}
}
image.png
②. 为什么要使用LockSupport
image.png线程等待唤醒机制(wait/notify)的改良加强版
IMG_2131(20201027-152517).JPG
①. 3种让线程等待唤醒的方法
使用Object中的wait()方法让线程等待,使用Object中的notify方法唤醒线程
使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
②. Object类中wait( )和notify( )实现线程的等待唤醒
wait和notify方法必须要在同步块或同步方法里且成对出现使用。 wait和notify方法两个都去掉同步代码块后看运行效果出现异常情况:
Exception in thread “A” Exception in thread “B”
java.lang.IllegalMonitorStateException
先wait后notify才可以(如果先notify后wait会出现另一个线程一直处于等待状态)
synchronized是关键字属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只能在同步块或方法中才能调用wait/notify等方法)
public class SynchronizedDemo {
//等待线程
public void waitThread(){
// 1.如果将synchronized (this){}注释,会抛出异常,因为wait和notify一定要在同步块或同步方法中
synchronized (this){
try {
System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"end....");
}
}
//唤醒线程
public void notifyThread(){
synchronized (this){
System.out.println("唤醒A线程....");
notify();
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(()->{
// 2.如果把下行这句代码打开,先notify后wait,会出现A线程一直处于等待状态
// try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
synchronizedDemo.waitThread();
},"A").start();
new Thread(()->{
synchronizedDemo.notifyThread();
},"B").start();
}
}
③. Condition接口中的await和signal方法实现线程等待和唤醒
(出现的问题和object中wait和notify一样)
public class LockDemo {
static Object object=new Object();
public static void main(String[] args) {
Lock lock=new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(()->{
//如果把下行这句代码打开,先signal后await,会出现A线程一直处于等待状态
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
condition.await();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"\t"+"END....");
},"A").start();
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程****");
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
},"B").start();
}
}
④. LockSupport详解
4>.
LockSupport详解
- ①. 什么是LockSupport?
- 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
- LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
- 官网解释:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零
可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
- ②. 阻塞方法
- permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时, park方法会被唤醒,然后会将permit再次设置为0并返回。
- static void park( ):底层是unsafe类native方法
- static void park(Object blocker)
- ③.唤醒方法(注意这个permit最多只能为1)
- 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回
-
static void unpark( )
image.png
- ④. LockSupport它的解决的痛点
- LockSupport不用持有锁块,不用加锁,程序性能好
- 先后顺序,不容易导致卡死(因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞)
- ⑤. 代码演示:
/*
(1).阻塞
(permit默认是O,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,
park方法会被唤醒,然后会将permit再次设置为O并返回)
static void park()
static void park(Object blocker)
(2).唤醒
static void unpark(Thread thread)
(调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,
permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回)
static void unpark(Thread thread)
* */
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
LockSupport.park();
/*
如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
所以下一行被注释的打开会导致程序处于一直等待的状态
* */
//LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
},"A");
t1.start();
//下面代码注释是为了A线程先执行
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
Thread t2=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
//有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
LockSupport.unpark(t1);
//LockSupport.unpark(t1);
},"B");
t2.start();
}
}
⑥. 面试题目:
- 为什么可以先唤醒线程后阻塞线程?(因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞)
- 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?(因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行)
⑤. AbstractQueuedSynchronizer之AQS --抽象的队列同步器
①. AQS是什么?
-
①. 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status值的修改(0表示没有,1表示阻塞)
image.png
- ②. AQS为什么是JUC内容中最重要的基石
(ReentrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore )
-
③. 锁,面向锁的使用者(定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可)
同步器,面向锁的实现者(比如Java并发大神Douglee,提出统一规 范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。) -
④. 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果
进一步理解锁和同步器的关系
锁 面向锁的使用者
同步器,面向锁的实现者
IMG_2132(20201027-165030).JPG
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
②. AQS内部体系架构
①. AQS内部架构图:
image.png
双向队列
image.png
image.png
-
②. 详解AQS内部代码有什么?
在这里插入图片描述 -
③. CLH队列(三个大牛的名字组成),为一个双向队列
在这里插入图片描述
AQS = state + CLH双向队列
-
④. 内部结构(Node此类的讲解)
在这里插入图片描述 -
⑤. 属性说明(Node此类的讲解)
- ⑥. AQS同步队列的基本结构
③. ReentrantLock开始解读AQS
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的。
IMG_2134(20201027-173915).JPG
②. 从最简单的lock方法开始看看公平和非公平
- ①. 通过ReentrantLock的源码来讲解公平锁和非公平锁
- ②. 可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
写在最前面:
(1). 本次讲解我们走最常用的,lock/unlock作为案例突破口
(2). 我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?3个状态:没占用是0,占用了是1,大于1是可重入锁
(3). 如果AB两个线程进来了以后,请问这个总共有多少个Node节点?答案是3个,其中队列的第一个是傀儡节点(哨兵节点)
业务图:
在这里插入图片描述
①. 代码展示:
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try{
System.out.println("-----A thread come in");
try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
}finally {
lock.unlock();
}
},"A").start();
//第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----B thread come in");
}finally {
lock.unlock();
}
},"B").start();
//第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----C thread come in");
}finally {
lock.unlock();
}
},"C").start();
}
}
③. lock()
- ①. lock.lock( ) 源码
-
②. acquire( ):源码和3大流程走向
在这里插入图片描述
④tryAcquire(arg)
-
①.本次走非公平锁方向
image.png
- ②. nonfairTryAcquire(acquires)
return false(继续推进条件,走下一步方法addWaiter)
return true(结束)
⑤. addWaiter(Node.EXCLUSIVE)
image.png- ①. addWaiter(Node mode )
- enq(node);
- 双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的
for(;;)代表无限循环的意思。if里面的语句上面那个图👆
在这里插入图片描述
-
②. enq(node);
在这里插入图片描述
⑥. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
-
①. acquireQueued
假如再抢抢失败就会进入如下方法:
shouldParkAterFailedAcquire和parkAndCheckInterrupt方法中
shouldParkAfterF ailedAcquire
parkAndCheckInterrupt
在这里插入图片描述 -
②. shouldParkAfterFailedAcquire
(如果前驱节点的waitstatus是SIGNAL状态(-1),即shouldParkAfterFailedAcquire方法会返回true 程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起)
在这里插入图片描述 -
③. parkAndCheckInterrupt
在这里插入图片描述
⑦. unlock( )获取permit
-
①. sync.release(1) 》 tryRelease(arg) 》unparkSuccessor 》杀回马枪
image.png - ②. tryRelease()
- ③. unparkSuccessor( )
网友评论