JAVA可重入锁ReentrantLock及其公平性

重入锁ReentrantLock

顾名思义,就是支持重复进入的锁,它表示该所能够支持一个线程对资源的重复加锁。而除此之外,该所还支持了获取锁时的公平和非公平性的选择。注意,ReentrantLock是一个独占锁。

重入性分析及实现

分析

这里对比一下synchronized:要知道,synchronized也是支持可重入的,只不过是隐式的,例如,一个synchronized关键字修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次的获取该锁。

ReentrantLock没有像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法的时候,已经获取该锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

实现

如果你对java线程并发与锁相关知识很熟悉的话,你会马上想到AbstractQueueSynchronizer,也就是队列同步器,以下简称AQS。我们知道,Lock()接口是面向使用者的,而AQS是面向实现者的。所以,ReentrantLock也不例外。他就是聚合了内部的一个自定义同步器,而这个自定义同步器又继承了AQS。

为了较好的理解,我们这样思考,假如现在没有可重入锁,我们自己实现这样一个锁,应该怎么实现这个可重入的逻辑呢?其实很简单,可重入的本质就是已经拿到同步状态(基于AQS的,如果你不了解AQS,需要先充电相关内容)的线程可以再次获取同步状态,想到这里,其实就很简单了,我们只需要在某一个线程获取到同步状态后,将该线程与同步状态绑定,之后所有线程尝试获取同步状态的时候,都要先判断该线程是不是同步状态绑定的线程。

ReentrantLock就是基于上述思想,只不过实现了更多的细节,我们来看一下JDK的源码

(这里以非公平性选择为例。)

仔细理解上述代码,发现:先判断同步状态为0时候,表示现在还没有线程获取,那就CAS原子性获取同步状态,成功后,绑定当前线程。else,也就是同步状态不为0,在此基础上,要判断当前请求线程是否为同步状态锁绑定的线程,如果是,同步状态数自增,设置新的同步状态,仍旧返回true,表示成功,也就不会阻塞。除上述两种情况外,一律返回false,交由上层的Acquire()方法去处理,也就是进入请求等待队列。

公平性选择的理解和实现

首先,一定要理解这样一个问题,我们的ReentrantLock既然是使用了底层的AQS,那自然我们重写tryAcquire(),然后使用来自AQS的模板方法acquire()去调用。我们也知道,我们重写的tryAcquire()方法,只能按照自己的逻辑意愿去控制每一次获取锁的逻辑,一旦获取失败,底层的AQS的acquire()方法会去处理。所以获取失败的处理逻辑永远是一致的,只要你使用了AQS,因为acquire()方法还是final的,不可重写。

那么,问题来了,AQS底层的请求等待队列的管理就是一个双向队列,且其处理逻辑就是FIFO(先来先出),也就是说,只要线程请求同步状态失败,那么进入到AQS的请求等待队列后,无论是公平性锁,还是非公平性锁,或是其他任何的锁,都是一样的处理逻辑,当然了,这所有的前提都是你使用了AQS,除非,你重写一个底层的处理框架。

所以,说这么多,就是为了理解一件事情,所谓的ReentrantLock的公平性选择的差异性,是体现在进入到AQS底层请求等待队列之前的,进入队列之后,那就只能规规矩矩按照FIFO的顺序了。

好了,基于上述概述,为了更好的理解,我们还是来看JDK的源码:

这里先文字描述一下整个ReentrantLock内部的层次关系:

首先有一个核心自定义同步器Syn,继承了AQS.

然后,基于公平性和非公平性的区别,又对应实现了两个自定义同步器,继承Syn。

然后,ReentrantLock有个构造函数,通过传入boolean来指定该锁的公平性选择,以使用对应的同步器。

无参默认构造函数,默认采用的是非公平性的同步器。

传参boolean构造函数,true公平性,false非公平性。

我们来看一下这两个自定义同步器的差异:

1.非公平性锁:

(1)

先记住此处的lock()方法先直接尝试了一次CAS获取同步状态

(2)

2.公平性锁

(3)

(4)

3.ReentrantLock的lock()方法:

(5)

分析总结:基于上述代码,我们发现重入锁的构造函数根据其传入参数,决定了其使用的自定义同步器,而使用可重入锁的时候,必然是调用其lock()方法,我们发现底层的lock()方法,其实是调用了所使用的自定义AQS的lock()方法。

而这时候就有区别了:我们知道lock()方法其实应该调用的是底层AQS的acquire()方法,所以既然锁使用了自定义AQS的lock()方法,那么在自定义AQS的lock()方法中,应该调用acquire()方法。回到代码中,我们发现公平性的自定义AQS的lock()方法直接使用了AQS的acquire()方法。而非公平性的lock()方法,则先直接进行了一次CAS的尝试获取,获取失败才调用AQS的acquire方法。

继续往下看,重写的tryAcquire()方法,公平锁和非公平锁的自定义AQS的tryAcquire()并没有什么不同,当然了,非公平锁的tryAcquire间接的又往下调用了一层nonfairTryAcquire()的真正实现。我们对比这两段代码,发现也就公平性锁的实现中多出了一条代码,其他的都一样。

公平锁多出的一条代码。

这调代码是干嘛的呢?简单的理解就是,当前是否有线程正在请求该同步状态或者底层的请求等待队列中是否有其他线程在等待。

哈哈,恍然大悟,所谓的公平性锁,和非公平性锁的区别:其实就在于这第一次请求锁时候的处理逻辑,公平性锁的线程在初次请求同步状态的时候,如果有线程比起更早的请求同步状态,他会直接返回false,进入请求等待队列。而非公平性锁,他在调用lock()方法的时候,就直接进行一次CAS尝试获取同步状态(非公平性的本质),而不理会是否有比其更早的线程请求同步状态,如果这一次CAS失败了,才继续执行对应的nonTryAcquire()。

公平性选择的总结:

线程使用ReentrantLock获取锁可以抽象的理解为两个阶段,第一个阶段是第一次尝试获取,第二个阶段是基于AQS底层所维护的请求等待队列的竞争。所谓的公平性与否,其实就是表现在第一次尝试获取上的区别,因为一旦进入了AQS的请求等待队列后,无论你是什么,后续的执行逻辑都是一样的。

猜你喜欢

转载自blog.csdn.net/romantic_jie/article/details/100205666