• 欢迎来到本博客,希望可以y一起学习与分享

可重入锁与LockSupport与AQS

Java benz 2个月前 (07-30) 31次浏览 0个评论 扫描二维码
文章目录[隐藏]

可重入锁

可重入锁(又名递归锁)
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一对象),不会因为之前已经获取过还没释放而阻塞。

一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。也即,自己可以获取自己的内部锁
Java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

可重入锁的种类

隐式锁(即synchronized关键字修饰的锁)默认是可重入锁,隐式锁自动加锁与释放锁;
显式锁(即Lock)也有ReentrantLock这样的可重入锁,显式锁手动加锁(Lock.lock();)与释放锁(Lock.unLock();)。

隐式锁

Synchronized的同步代码块:

Synchronized的同步方法:

Synchronized重入的实现机制

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则,需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放。

显式锁:加锁几次就要解锁几次

LockSupport

什么是LockSupport

官方文档:java.util.concurrent.locks.LockSupport

中文:java.util.concurrent.locks.LockSupport

LockSupport是用于创建锁和其他同步类的基本线程阻塞原语。核心的方法是park()阻塞线程;unpark()解除阻塞线程。

一句话:LockSupport就是对线程唤醒机制(wait/notify)的升级加强版

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

方式一:使用Object中的wait()方法让线程等待,notify()方法唤醒线程。如:synchronized。
方式二:使用JUC包中Condition的await()方法让线程等待,signal()方法唤醒线程。如:ReentrantLock。
方式三:使用LockSupport类的park()方法让线程等待,unpark()方法唤醒线程。

方式一

去掉synchronized


notify()与wait()执行顺序互换,先执行notify()后执行wait()

 

总结

1、Object类中的wait()notify()notifyAll(),都必须在synchronized内部执行(必须用到关键字synchronized)。

2、notify()先于wait()执行,程序会无法执行,等待中的线程无法唤醒。

方式二

去掉lock()

await()与signal()执行顺序互换,先执行signal()后执行await()

总结

1、Condition中的await()signal()signalAll(),都必须在lock()unlock()之间执行。

2、signal()先于await()执行,程序会无法执行,等待中的线程无法唤醒。

LockSupport的park()与unpark()

park()的源码如下:

UNSAFE.park(false, permit);permit(许可证),它的值只有0和1。
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park()方法会被唤醒,然后会将permit再次设置为0并返回。
unpark(Thread thread)的源码如下:

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

结果

先执行unpark(),后执行park()

结果:没有阻塞

重点说明

LockSupport是用来创建锁和其它同步类的基本线程阻塞原语。
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。归根结底,LockSupport调用Unsafe中的native代码。

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

形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

LockSupport:俗称 锁中断
以前的两种方式:
1、以前的等待唤醒通知机制必须synchronized里面有一个wait和notify
2、lock里面有await和signal
这上面这两个都必须要持有锁才能干,且如果先执行了notify或者signal那么执行wait或者await时会一直等待下去,lockSupport解决了这一点。
LockSupport它的解决的痛点
1、LockSupport不用持有锁块,不用加锁,程序性能好,
2、先后顺序,不容易导致卡死

面试题

为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够,不能放行。

 

AQS

它在哪里

在IDEA下,点击左边的ReentrantLock

可以发现,抽象类Sync ,它继承了一个抽象类:AbstractQueuedSynchronizer,AQS就是这个抽象类的缩写。

中文文档:java.util.concurrent.locks.AbstractQueuedSynchronizer

什么是AQS

AQS是 AbstractQueueSynchronizer 的缩写。AbstractQueueSynchronizer其自身是抽象类,提供模板,供子类实现。

字面意思:抽象的队列同步器。
技术解释:是用来构建锁或者其他同步组件的基础框架及整个JUC体系的基石,它使用了一个int成员变量表示持有锁状态,通过内置的FIFO队列(双向队列)来完成资源获取线程的排队工作。

抢到资源的线程直接使用,处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候的线程仍然保留获取锁的可能且获取锁的流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的控制效果。

 

ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore等JUC底层都继承了AbstractQueueSynchronizer 这个抽象类。

AQS体系结构

AQS = state + CLH变种的双端队列

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的双向队列,对每一个抢占资源的线程封装成一个个Node节点,来实现锁的分配;通过CAS完成对State值的修改。
AQS内部体系架构

Node节点结构

Node = waitStatus + 前后指针指向

AbstractQueuedSynchronizer有个内部类Node,这个Node的元素是Thread,相当于Node< Thread >。这个与Map类似,Map是通过数组实现的,数组的元素时Node< k, v>。
其中waitStatus是排队队列中每个线程的状态,相当于定后驱每个客户的等待状态。取值为上面的枚举值。
Node类的属性含义

AQS同步队列的基本结构

AQS源码

从ReentrantLock入手,去看AQS源码

从上面ReentrantLock源码,可知ReentrantLock的lock()是由sync.lock()完成的;unlock()是由sync.release(1)操作的,而sync则是继承了AbstractQueuedSynchronizer这个抽象类。

从最简单的lock()方法开始看,公平锁与非公平锁

在java.util.concurrent.locks.ReentrantLock.java,源码有公平锁与非公平锁。

可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了个限制条件:hasQueuedPredecessors()hasQueuedPredecessors()是公平锁加锁时判断等待队列中是否存在有效节点的方法。
hasQueuedPredecessors源码如下:

对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少一个判断!hasQueuedPredecessors()
hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

AQS源码分析

带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制。

AQS关键的方法:
lock():
acquire()
tryAcquire(arg)
addWaiter(Node.EXCLUSIVE)
acquireQueued(addWaiter(Node.EXCLUSIVE),arg)

ReentrantLock – > lock()

这个是获取锁的入口。sync是一个实现了AQS的抽象类,这个类的主要作用是用来实现同步控制的,sync有两个实现,一个是NonfairSync(非公平锁)、另一个是FailSync(公平锁)。
ReentrantLock – > NonfairSync.lock

当锁被占用时,执行acquire(1),以下都是对这个方法的详细分析

AQS – > tryAcquire

该方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构造成一个Node节点,并将节点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。

AQS – > nofairTryAcquire

AQS – >addWaiter

当前线程来请求锁的时候,如果锁已经被其他线程锁持有,当前线程会进入这个方法,这个方法主要是把当前线程封装成node,添加到AQS的链表中。

AQS- >enq

enq就是通过自旋操作把当前节点加入到队列中。

AQS- >acquireQueued

前面addWaiter返回了插入的节点,作为acquireQueued方法的入参,这个方法主要用于争抢锁。

原来的head节点释放锁以后,会从队列中移除,原来head节点的next节点会成为head节点。

AQS- >shouldParkAfterFailedAcquire

从上面的分析可以看出,只有队列的第二个节点可以有机会争用锁,如果成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点,if (p == head)条件不成立,首先进行shouldParkAfterFailedAcquire(p, node)操作。

shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,是说明此节点已经将状态设置-如果锁释放,则应当通知它,所以它可以安全的阻塞了,返回true。

AQS- >parkAndCheckInterrupt

如果shouldParkAfterFailedAcquire返回了true,acquireQueued会继续执行parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。

ReentrantLock – > unlock()

下面是释放锁的过程。先调用AQS – > release方法,这个方法里面做两件事,1.释放锁 ;2.唤醒park的线程。

ReentrantLock – > Sync.tryRelease

这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。

lock与unlock过程总结
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

参考

Lock原理分析
Java高并发学习(七)——AQS及源码解析


文章 可重入锁与LockSupport与AQS 转载需要注明出处
喜欢 (0)

您必须 登录 才能发表评论!