juc并发包里面有许多支持高并发的锁实现,其原理都是基于aqs实现,其中的可重入锁ReentrantLock只是其中的一种

AQS简介

AQS全称AbstractQuenedSynchronizer,意为抽象的队列同步器,其通过一个CLH队列(虚拟双向队列)来维护并发等待的线程节点,这是一个FIFO队列。总的来说,就是在并发时,获取锁失败的线程节点会进入到这个队列里面,待满足了某种条件之后在从这队列里面出来唤醒线程,下面先看看java底层中aqs的类定义与结构

首先,打开juc的源码可以看到AbstractQuenedSynchronizer继承了另一个抽象类AbstractOwnableSynchronizer,打开这个抽象类,可以看到其定义了一个禁止序列化的线程对象exclusiveOwnerThread,这里稍做解释一下,这个成员变量就来表示当前占用者资源的线程对象

介绍完其抽象父类后,我们再来看看AQS内部定义的2个内部类Node和ConditionObject,然后再去看AQS主要的方法

Node:节点对象
Node节点有2个用来标记节点类型的成员变量Shared和Exclusive,用来区分节点模型,应用到锁上时可表示共享锁或者独占锁,Thread表示这个节点的线程,prev和next是用来维护CLH队列时的双向节点链接引用,nextWaiter是维护条件等待队列单向引用的节点链接引用,其余的还包含表示节点状态的final int类型变量

ConditionObject:条件对象
成员变量firstWaiter和lastWaiter用来表示条件等待队列里面的头和尾节点,其余的是await和signal之类的等待与唤醒节点操作的方法

acquire方法(独占模式加锁,对应的共享模式也有acquireShared)
这个方法的入参是一个资源数,即需要锁定多少资源,可看作一个计数,对于可重入锁,第一次锁定时资源数是1,下次锁占有线程重入加锁时,锁定资源数也会加1,最后释放锁的时候资源数需要释放到0,才会将aqs的exclusiveOwnerThread释放掉,看源码这个方法会先调用tryAcquire尝试加锁,如果尝试加锁失败,则调用addWaiter增加一个独占锁的节点Node并调用acquireQueued添加到CLH队列排队

TryAcquire以及TryRelease方法
查看aqs的这2个方法,可以看到其是直接抛出了异常,所以这2个方法需要由子类自己实现,如果子类没有实现的话调用到这2个方法就会直接抛出异常

ReentranLock简介

简单介绍完AQS和Condition的概念之后,先来看看可重入锁的类定义和结构后面再看加锁和释放锁的内容,打开源码可以先看到ReentranLock内部定义了3个内部类,一个是抽象类Sync同步器,同步器定义了一个抽象的lock方法用来表示加锁操作,另外2个是继承了Sync实现lock接口的NonfairSync(非公平锁)和FairSync(公平锁),这2个锁的具体逻辑稍后在看,现再回到看ReentranLock的构造方法,可发现定义了2个构造器,一个无入参,另一个有个布尔值入参,可以看出可重入锁默认使用的是非公平锁,如果要使用公平锁可以使用另一个构造器初始化

NonfairSync非公平锁同步器

先来看非公平锁,它的lock接口首先做了一次cas原子操作尝试获取锁(将status从0设置为1),如果哪个线程cas操作成功了,那个线程就是当前锁的持有线程,需要将线程设置到aqs的exclusiveOwnerThread属性中,cas操作失败的其他线程调用acquire,由AQS部分的分析可知,acquire会先调用tryAcquire方法,且tryAcquire方法由子类实现,即调用NonfairSync自己的tryAcquire方法,继续跟进可以看到调用到抽象父类的Sync的nonfairTryAcquire方法。

所以总结一下,nonfairSync的加锁过程如下
1.首先是cas抢锁
2.抢锁失败的调用nonfairTryAcquire方法尝试获取锁

nonfairTryAcquire方法
查看这个方法的源码,可以看到如果当前的锁资源已经释放为0,则会再通过一次cas抢锁,如果当前锁资源不为0,则判断当前线程是否是锁持有线程,如果是,则可重入,以及锁资源数增加,否则返回false表示尝试获取锁失败,然后会回到acquire方法的位置,调用addWaiter等方法,将当前尝试获取锁失败的线程节点加入到CLH队列

FairSync公平锁同步器

再来看看公平锁的加锁方式,首先还是lock方法,这个方法此时直接调用了acquire方法,即转到tryAcquire方法尝试获取锁,查看公平锁的tryAcquire方法如下
1.如果锁资源为0,先调用hasQueuedPredecessors判断当前节点是否是的CLH队列里排在第一位的等待节点,即当前线程前面有没有在排队的节点
–有,等待节点则直接返回false,尝试获取锁失败,添加到CLH队列排队
–无,尝试cas抢锁
2.如果锁资源不为0,则判断当前进入线程是否是锁持有线程
–是,则让其重入加锁,资源数增加
–否,返回false,尝试获取锁失败,添加到CLH队列排队

NonfairSync非公平锁与FairSync公平锁的区别

通过上述的对比可以看出公平锁和非公平锁的主要区别在于加锁的过程中

非公平锁加锁,在资源数为0的情况下,直接通过cas进行抢锁

公平锁加锁,在资源数为0的情况下,先判断CLH队列是否有排队的节点且队列的第一个节点线程非当前线程
–有:直接返回false,当前线程到CLH队列中排队
–无:尝试cas抢锁

可以看出,公平锁就是多了一个判断CLH队列的流程来保证在资源数为0时抢锁的公平性的

ReentranLock.unlock

最后再来看看解锁操作,查看unlock接口,可看到其直接调用了aqs的release方法,这个方法继续转到调用tryRelease方法尝试释放锁资源,如果释放成功会继续判断当前的CLH队列的头节点是否是等待节点,是的话还会调用unparkSuccessor唤醒next节点,这部分是ConditionObject条件相关的机制流程,源码也比较简单这里就不多说了,回到tryRelease,这个方法上面说过是由子类实现,所以查看可重入锁的3个内部类得知,实现方法在内部抽象类Sync中

释放资源过程如下
1.判断当前线程是否是锁持有线程
–是,则根据入参是否指定资源数,当资源数释放后为0时将aqs的exclusiveOwnerThread也设置为null
–否,则抛出异常

总结

关于AQS的简介以及其实现类ReentranLock可重入锁的分析暂时就到这里,另外关于可重入锁用的比较多的是配合Condition一起使用,所以本文只是简要的提一下概念,感兴趣的同学可以区看看juc的相关源码,比较容易理解。

发表评论

电子邮件地址不会被公开。