可重入锁的理解及公平锁和非公平锁的具体实现

2022年12月3日08:26:51

可重入锁

首先结合以下两个例子理解以下可重入锁的概念。

/**
 * 可重入锁:
 * 1、可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
 * 2、是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个
 * 对象),不会因为之前已经获取过还没释放而阻塞
 */publicclassReEnterLockDemo{staticObject objectLockA=newObject();publicstaticvoidm1(){newThread(()->{synchronized(objectLockA){System.out.println(Thread.currentThread().getName()+"\t"+"------外层调用");synchronized(objectLockA){System.out.println(Thread.currentThread().getName()+"\t"+"------中层调用");synchronized(objectLockA){System.out.println(Thread.currentThread().getName()+"\t"+"------内层调用");}}}},"t1").start();}publicstaticvoidmain(String[] args){m1();}}
publicclassReEnterLockDemo{publicsynchronizedvoidm1(){System.out.println("=====外层");m2();}publicsynchronizedvoidm2(){System.out.println("=====中层");m3();}publicsynchronizedvoidm3(){System.out.println("=====内层");}publicstaticvoidmain(String[] args){newReEnterLockDemo().m1();}}

相关知识的了解

3种让线程等待和唤醒的方法

  • 方式1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
  • 方式2: 使用JUC包中Conditionawait()方法让线程等待,使用signal()方法唤醒线程
  • 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object类提供的等待唤醒机制的缺点

publicclassLockSupportDemo1{staticObject objectLock=newObject();publicstaticvoidmain(String[] args){newThread(()->{synchronized(objectLock){System.out.println(Thread.currentThread().getName()+"\t"+"------come in");try{
                    objectLock.wait();}catch(InterruptedException e){
                    e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");}},"A").start();newThread(()->{synchronized(objectLock){
                objectLock.notify();System.out.println(Thread.currentThread().getName()+"\t"+"------通知");}},"B").start();}}

结果:

A	------come in
B	------通知
A	------被唤醒

Process finished with exit code 0

异常情况①:去掉同步代码块

publicclassLockSupportDemo1{staticObject objectLock=newObject();publicstaticvoidmain(String[] args){newThread(()->{//            synchronized (objectLock){System.out.println(Thread.currentThread().getName()+"\t"+"------come in");try{
                    objectLock.wait();}catch(InterruptedException e){
                    e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");//            }},"A").start();newThread(()->{//            synchronized (objectLock){
                objectLock.notify();System.out.println(Thread.currentThread().getName()+"\t"+"------通知");//            }},"B").start();}}

结果:

A	------come in
Exception in thread "A" Exception in thread "B" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$0(LockSupportDemo1.java:16)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$1(LockSupportDemo1.java:26)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

异常情况②:先唤醒,再等待。

publicclassLockSupportDemo1{staticObject objectLock=newObject();publicstaticvoidmain(String[] args){newThread(()->{try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedException e){
                e.printStackTrace();}synchronized(objectLock){System.out.println(Thread.currentThread().getName()+"\t"+"------come in");try{
                    objectLock.wait();}catch(InterruptedException e){
                    e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");}},"A").start();newThread(()->{synchronized(objectLock){
                objectLock.notify();System.out.println(Thread.currentThread().getName()+"\t"+"------通知");}},"B").start();}}

结果:

B	------通知
A	------come in

Process finished with exit code -1

死循环,A无法被唤醒了。

两种异常:

Object类提供的waitnotify

1、只能在synchronized同步代码块里使用

2、只能先等待(wait),再唤醒(notify)。顺序一旦出错,那个等待线程就无法被唤醒了。

Condition类提供的等待唤醒机制的缺点

这里也有两个缺点,而且和Object类里的wait,notify几乎一样。

1、只能在lock同步代码块里使用,不然就报错

2、只能先等待(await),再唤醒(signal)。顺序一旦错了,那个等待线程就无法被唤醒了。

但相对于wait,notify改进的一点是,可以绑定lock进行定向唤醒,或者说精确唤醒

LockSupport(本节重点)

首先直接看示例

异常情况①:无同步代码块

publicclassLockSupportDemo3{publicstaticvoidmain(String[] args){/**
         LockSupport:俗称 锁中断
         LockSupport它的解决的痛点
         1。LockSupport不用持有锁块,不用加锁,程序性能好,
         2。不需要等待和唤醒的先后顺序,不容易导致卡死
         */Thread t1=newThread(()->{System.out.println(Thread.currentThread().getName()+"\t ----begin-时间:"+System.currentTimeMillis());LockSupport.park();//阻塞当前线程System.out.println(Thread.currentThread().getName()+"\t ----被唤醒-时间:"+System.currentTimeMillis());},"t1");
        t1.start();LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName()+"\t 通知t1...");}}

结果:

t1	 ----begin-时间:1603376148147
t1	 ----被唤醒-时间:1603376148147
main	 通知t1...

Process finished with exit code 0

没有出现任何问题。

异常情况②:先唤醒,再阻塞(等待)。

publicstaticvoidmain(String[] args){Thread t1=newThread(()->{try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedException e){
                e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t ----begin-时间:"+System.currentTimeMillis());LockSupport.park();//阻塞当前线程System.out.println(Thread.currentThread().getName()+"\t ----被唤醒-时间:"+System.currentTimeMillis());},"t1");
        t1.start();LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName()+"\t 通知t1...");}

结果:

main	 通知t1...
t1	 ----begin-时间:1603376257183
t1	 ----被唤醒-时间:1603376257183

Process finished with exit code 0

可以看到,如果提前对线程进行唤醒。那么后面执行的LockSupport.park();就相当于瞬间被唤醒了,不会和之前一样程序卡死。

为什么呢?

staticvoidpark()//除非许可证可用,否则禁用当前线程以进行线程调度。staticvoidunpark(Thread thread)//如果给定线程尚不可用,则为其提供许可。
  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),
    permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
publicstaticvoidpark(){
        UNSAFE.park(false,0L);}

LockSupport底层是通过UNSAFE。

  • permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。

  • 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(多次调用unpark方法,不会累加,permit值还是1),然后程序就会自动唤醒thread线程,也就是之前阻塞中的LockSupport.park()方法会立即返回。

  • LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就将0变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
    每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。

  • 简单的理解就是,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

    当调用park方法时:
    如果有凭证,则会直接消耗掉这个凭证然后正常退出;
    如果无凭证,就必须阻塞等待凭证可用。
    而unpark则相反:
    它会增加一个凭证,但凭证最多只能有1个,累加无效。

    因此上述问题的答案就是:

1、先执行unpark(),将许可证由0变为1。

2、然后park()来了发现许可证此时为0(也就是有许可证),那么他就不会阻塞,马上就往后执行。同时消耗许可证(也就是将1又变为0)。

回归正题

Java中的synchronizedReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁

先看下ReentrantLock的继承关系图:
可重入锁的理解及公平锁和非公平锁的具体实现

ReentrantLock实现了Lock接口,对外提供Lock接口的方法。有一个同步器属性,上锁、释放锁都是通过调用同步器的相关方法实现的。构造时,同步器可以选择公平锁/非公平锁,它们都继承了抽象父类Sync,而Sync又继承了AQS
抽象类AQS维护了等待队列,而ReentrantLock只需要定义共享资源的获取与释放的方式。

可重入功能的实现原理

ReentrantLock的可重入功能基于AQS的同步状态:state。
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。
以下是非公平锁(默认下我们使用的都是非公平锁)中可重入的实现原理。

publicReentrantLock(){// 默认非公平锁
    sync=newNonfairSync();}publicReentrantLock(boolean fair){
    sync= fair?newFairSync():newNonfairSync();}

这是非公平锁的部分代码,这里只为说明可重入的实现原理(详见注释),对于非公平锁更详细分析见后文。

// acquires的值是1finalbooleannonfairTryAcquire(int acquires){// 获取当前线程finalThread current=Thread.currentThread();// 获取state的值int c=getState();// 如果state的值等于0,表示当前没有线程持有锁// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回trueif(c==0){if(compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);returntrue;}}// state的值不等于0,表示已经有其他线程持有锁// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true// 如果不是当前线程,获取锁失败,返回falseelseif(current==getExclusiveOwnerThread()){int nextc= c+ acquires;if(nextc<0)// overflowthrownewError("Maximum lock count exceeded");setState(nextc);returntrue;}returnfalse;}

ReentrantLock中非公平和公平的实现原理

因为ReentrantLock同时支持公平锁和非公平锁,上文也提到,ReentrantLock默认无参构造函数使用的是非公平锁,有参构造函数可指定使用公平锁。
对于公平锁:是指在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会从尾结点加入队列。
对于非公平锁:就是在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁,这里其实是一种插队的表现。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。此时,队列中被唤醒的线程并不一定能优先获得锁,当队列中被唤醒的线程被新来的线程抢占了资源,这种插队也就表现出了非公平的特性

非公平锁的获取

注意和后面公平锁的对比

staticfinalclassNonfairSyncextendsSync{finalvoidlock(){// 和公平锁相比,这里会直接先进行一次CAS(尝试插队),成功就返回了。这是第一处不一样【对比请看下方公平锁的lock】if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// AbstractQueuedSynchronizer类的acquire(int arg)方法publicfinalvoidacquire(int arg){if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protectedfinalbooleantryAcquire(int acquires){returnnonfairTryAcquire(acquires);}}// acquires的值是1finalbooleannonfairTryAcquire(int acquires){// 获取当前线程finalThread current=Thread.currentThread();// 获取state的值int c=getState();// 如果state的值等于0,表示当前没有线程持有锁// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回trueif(c==0){// 这里没有对队列进行判断,直接CAS抢,这是第二点不一样【对比请看下方公平锁的lock】if(compareAndSetState(0, acquires)){//获取成功就设置线程变量setExclusiveOwnerThread(current);returntrue;}}// state的值不等于0,表示已经有其他线程持有锁// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true// 如果不是当前线程,获取锁失败,返回falseelseif(current==getExclusiveOwnerThread()){int nextc= c+ acquires;// 如果小
  • 作者:weixin_47365232
  • 原文链接:https://blog.csdn.net/weixin_47365232/article/details/124354566
    更新时间:2022年12月3日08:26:51 ,共 8562 字。