上一篇文章已经分析了公平锁的获取与释放,本篇文章在前文的基础上分析非公平锁的获取与释放。如果你看懂跑了前面公平锁的获取与释放主要流程,那么看懂本篇文章将会比较轻松。
获取非公平锁
非公平锁和公平锁在获取锁的方法上,流程是一样的;它们的区别主要表现在“尝试获取锁的机制不同。简单点说,公平锁在每次尝试获取锁时,都是采用公平策略(根据等待队列依次排序等待);而非公平锁在每次尝试获取锁时,都是采用的非公平策略(无视等待队列,直接尝试获取锁,如果锁是空闲的,即可获取状态,则获取锁)。
在前面的java线程系列 JUC锁 03 ReentrantLock公平锁”中,已经详细介绍了获取公平锁的流程和机制;下面,通过代码分析以下获取非公平锁的流程。
lock
lock()在ReentrantLock.java的NonfairSync类中实现,它的源码如下:
1 | final void lock() { |
lock()会先通过compareAndSet(0, 1)来判断锁是不是空闲状态。是的话,当前线程直接获取锁;否则的话,调用acquire(1)获取锁,主要流程如下
- 通过compareAndSetState()函数设置当前锁的状态。若锁的状态值为0,则设置锁的状态值为1。也就是获取锁成功。然后通过
setExclusiveOwnerThread(Thread.currentThread())
设置当前线程为锁的持有者,这样就获取锁成功。 - 如果上面失败,则通过acquire(1)来获取锁
公平锁和非公平锁关于lock()的对比
- 公平锁 – 公平锁的lock()函数,会直接调用acquire(1)。
- 非公平锁 – 非公平锁会先判断当前锁的状态是不是空闲,是的话,就不排队,而是直接获取锁。
acquire()
acquire()在AQS中实现的,它的源码如下:
1 | public final void acquire(int arg) { |
- 当前线程首先通过tryAcquire尝试获取锁,如果获取成功的话,直接返回,尝试失败,就要进入下一步。
- 当前线程获取失败,通过addWaiter(Node.EXCLUSIVE)将当前线程插入到CLH队列末尾来等待获取锁。
- 插入成功后,会使用acquireQueued来获取锁,这里获取锁只会等待当前等待节点的前继节点为head节点才会获取成功。没有获取锁,线程会进入休眠状态。如果当前线程在休眠等待过程中被打断,acquireQueue会返回true,此时当前线程会调用selfInterrupt来给自己产生一个中断。
公平锁和非公平锁关于acquire()的对比
公平锁和非公平锁,只有tryAcquire()函数的实现不同;即它们尝试获取锁的机制不同。这就是我们所说的它们获取锁策略的不同所在之处。在前文中,已经详细介绍了acquire()涉及到的各个函数。这里仅对它们有差异的函数tryAcquire()进行说明。
tryAcquire
非公平锁的tryAcquire()在ReentrantLock的NonfairSync类中实现,源码如下:
1 | protected final boolean tryAcquire(int acquires) { |
nonfairTryAcquire()在ReentrantLock的Sync类中实现,源码如下:
1 | // sync |
根据代码,tryAcquire()的作用就是尝试去获取锁。
- 如果锁没有被任何线程拥有,则通过CAS函数设置锁的状态为已被获取状态,同时,设置当前线程为锁的持有者,然后返回true。
- 如果锁的持有者已经是当前线程,则将更新锁的状态即可。
- 如果不是上面的两种情况,则认为尝试获取锁失败。
公平锁和非公平锁关于tryAcquire()的对比
- 公平锁在尝试获取锁时,即使锁没有被任何线程锁持有,它也会判断自己是不是CLH等待队列的表头;是的话,才获取锁。
- 而非公平锁在尝试获取锁时,如果锁没有被任何线程持有,则不管它在CLH队列的何处,它都直接获取锁。
至于非公平锁的释放和公平锁是一样的,这里就不具体说明,下面主要分析ReentrantLock还剩下和获取锁有关的几个函数。
lockInterruptibly和tryLock分析
lockInterruptibly
这个函数和lock区别是,他响应中断,也就是当等待获取锁的线程在等待获取锁的时候收到中断信号,此方法会抛出中断异常。具体看下面源码
1 | public void lockInterruptibly() throws InterruptedException { |
从上面可以看出,主要的逻辑是在AQS中的acquireInterruptibly,至于参数1和前面的公平锁的参数一样,这里就不解释。下面看看上面的具体逻辑。
- 判断线程是否被中断,如果被中断,抛出中断异常
- 尝试获取锁,如果ReentrantLock使用的是公平锁,则使用的是公平锁的获取流程,否则是非公平锁的获取流程。获取成功,直接返回,失败则进入下一步。
- 使用doAcquireInterruptibly获取锁。
doAcquireInterruptibly
获取锁的源码如下:
1 | private void doAcquireInterruptibly(int arg) |
上面代码和acquireQueued方法的对比,唯一的区别就是:当调用线程获取锁失败,进入阻塞后,如果线程被中断,acquireQueued只是用一个标识记录线程被中断过,而doAcquireInterruptibly则是直接抛出异常。其他的是一样。具体可以看前面。
tryLock
这个方法是尝试获取锁,成功则返回true,失败返回false。有俩个版本,一个是带有时间的等待,一个不带。下面看看俩者的区别。
首先来看看不带超时时间的tryLock,源码如下
tryLock
1 | public boolean tryLock() { |
从上面可以看出,不管当锁的类型是公平和非公平,都是使用nonfairTryAcquire来获取锁,在前面我们分析非公平锁的获取时已经分析了这部分的内容。这里就不具体讲解。
tryLock(long timeout, TimeUnit unit)
这个是带有等待时间的获取锁的版本,如果第一次尝试获取锁失败,则等待指定的时间,在此尝试获取锁,如果成功则返回true,失败返回false。
1 | public boolean tryLock(long timeout, TimeUnit unit) |
下面看看上面的具体流程
- 判断当前线程是否被中断,如果中断抛出中断异常
- 先通过tryAcquire尝试获取锁,如果成功,返回true,失败返回false。
- 如果前面获取失败,通过doAcquireNanos来获取锁。
下面具体看看上面doAcquireNanos的流程,源码如下
1 | private boolean doAcquireNanos(int arg, long nanosTimeout) |
上面流程还是比较清晰,下面总结上面的流程。
- 判断等待的时间如果小于等于0,则直接返回false
- 计算等待的截止时间,并将当前节点插入到等待队列中。
- 进入循环
- 判断当前节点是否能成功获取锁,如果成功获取则返回true
- 计算等待的时间,如果小于0,返回失败
- shouldParkAfterFailedAcquire判断当前节点是否阻塞,如果返回false,进入新一轮的循环。
- 上一步如果返回true,则判断还需等待的时间是否小于spinForTimeoutThreshold,如果小于则不等待,进入自旋。这是一个优化,因为线程的切换需要时间,如果阻塞的时间非常短,则可以进入自旋,从而提升整体的性能。如果大于,则进入有限时间的阻塞。
- 判断线程是否中断过,如果是,则抛出中断异常
- 进入finally,如果failed=true,则取消当前节点。