JDK 源码 - ReentrantLock
前面已经或粗或细地看过了 AbstractQueuedSynchronizer 的源码,不妨趁热打铁把其常见的实现的源码也翻一遍,于是我们先来看 ReentrantLock
内部是怎么实现的。
一. 简介
ReentrantLock
顾名思义是一个可重入锁,它是基于 AbstractQueuedSynchronizer
实现的,支持公平锁和非公平锁机制。它实现了 java.util.concurrent.locks.Lock
以及 java.io.Serializable
接口。
|
|
它内部持有了一个 Sync
内部抽象类对象,加锁和解锁的逻辑转发具体的 Sync
子类方法上实现,有点类似 Mybatis
里面Session
与 Executor
间用到的门面设计模式。
|
|
Sync
其实是一个 AbstractQueuedSynchronizer
抽象实现,其中实现了AbstractQueuedSynchronizer
中的部分方法,其子类 FairSync
和 NonfairSync
也实现了部分方法,剩下的继承 AbstractQueuedSynchronizer
中的实现,下面是相关的类关系图。
顾名思义,NonfairSync
是非公平锁实现,FairSync
是公平锁实现,而公平锁与非公平的区别只在当前线程获取锁是否需要排队这一判断,下面说到加锁的时候会提及。
先看 ReentrantLock
的构造函数,默认会创建一个 NonfairSync
类型的 Sync
实现,也就是说 ReentrantLock
默认提供的是非公平锁。
|
|
二. 非公平锁
非公平锁获取锁时不需要考虑当前线程是否需要排队问题,任何尝试获取锁的线程都可以直接进行通过 CAS
操作,因为如果此刻有线程在排队,而后来者却可以先于它们获取到锁,即 ”先到不一定先得“,所以称之为非公平锁,这种机制可能导致 ”线程饿死“ 的情况出现,即排队的线程可能非常倒霉一直被人插队,自己一直拿不到锁,对应的任务也迟迟不能执行。
1. 获取锁
NonfairSync
中只重写了获取锁相关的方法,默认地,调用 ReentrantLock
的 lock 方法,将调用 NonfairSync
的 lock 方法,其中不会判断当前队列情况,而是在第一时间 compareAndSetState
,如果加锁成功,则当前线程成为独占线程。
|
|
如果加锁失败,则会调用 AbstractQueuedSynchronizer
中的 acquire
模板方法,其中又会回调 NonfairSync
中的 tryAcquire
方法,而 tryAcquire
则会复用 Sync
中的 nonfairTryAcquire
方法。
|
|
所以实际的非公平尝试加锁逻辑在 nonfairTryAcquire
,其中主要的判断当前锁是否可获取
- 如果是则尝试
compareAndSetState
,将 state 从 0 改成 1 进行加锁 - 如果锁已经被其他线程占有,则判断是否是当前线程占有的,是则重入
|
|
如果加锁失败,则会执行 AbstractQueuedSynchronizer
的 acquire 方法中 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
代码,这块的执行过程在前面介绍 AbstractQueuedSynchronizer
时已经讲过,大概流程就是将当前线程节点化入队然后判断是否需要阻塞,是则将前继节点的 waitSttatus
改成 -1,然后自己阻塞。
2. 释放锁
非公平锁释放锁调用的是 AbstractQueuedSynchronizer
中的模板方法 release
,其中会回调 Sync
中的钩子函数 tryRelease
,如果tryRelease
返回成功,表示释放锁成功,则会判断队列是否初始化,是的话则尝试唤醒队列中的第一个非哨兵非取消节点。
|
|
再看 release
方法,逻辑比较简单,如果 state 被减为 0,则将独占线程指针置空返回 true。
|
|
三. 公平锁
公平锁加锁是直接调用 AbstractQueuedSynchronizer
的 acquire 方法,这样能复用入队和阻塞的逻辑,至于自己的定制化逻辑,则由钩子函数 tryAcquire
实现。
|
|
与非公平锁不同,公平锁在 tryAcquire
中需要判断此刻是否有线程在排队,如果没有则直接 CAS
获取锁,获取成功则设置当前线程为独占线程,返回 true 结束;如果当前有线程在排队,则由于要实现公平,遵循先到先得原则,则当前线程也需要排队,所以直接返回 false,回到 AbstractQueuedSynchronizer
里面的 acquire 方法中执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
代码即可。
|
|
hasQueuedPredecessors
的判断极其复杂,需要考虑的场景有很多,在此先记住这个方法的返回值就如方法名所说的,如果返回 true,就表示有线程在等待,当前线程也要排队,否则说明没线程在排队,当前线程可以直接尝试获取锁。
-
返回 true,表示当前有线程在排队:
-
返回 false,表示当前没有有线程在排队:
- h == t:
- 队列没有初始化,h 和 t 都为 null,返回 false,如果
CAS
还能省掉初始化队列的操作。 - 队列初始化了,但是 h 和 t 都指向一个节点,这种情况只能是都指向哨兵节点,有可能是队列已经初始化了一段时间,排队的节点陆续出队,最后只剩下哨兵节点,由于不存在其他非哨兵节点,所以不需要排队,返回 false。
- 队列没有初始化,h 和 t 都为 null,返回 false,如果
h != t && ((s = h.next) != null && s.thread == Thread.currentThread())
:队列已经初始化,并且哨兵的后继节点不为空,并且后继节点的线程是当前线程,说明当前线程已经在排队,那么就不需要再排队,直接调用CAS
获取锁。
- h == t:
上面是公平锁加锁的大致过程,至于解锁过程则和非公平锁是一样,都是复用 Sync
中的 release
方法。
四. Q&A
1. hasQueuedPredecessors
当线程发现自己在排队是什么场景?会添加重复节点吗?
在 hasQueuedPredecessors
中存在一种情况是,当满足 h != t && ((s = h.next) != null && s.thread == Thread.currentThread())
时,将会返回 false,说明队列中只有当前线程在排队,不需要等待,直接尝试 CAS
,那么队列中是不是会有两个当前线程的节点呢?
我在网上看到 这篇文章,这个问题便出自其中,但是我看这个交流,我感到疑惑,首先当第一个非哨兵节点被唤醒时,它是不可能 CAS
失败的,因为这是公平锁,不可能存在其他线程与之竞争,由此我感觉楼主的解答有问题,但是我自己也产生了一个疑问,到底什么场景下一个线程进入 hasQueuedPredecessors
方法会发现自己在排队呢?
带着疑问看到 国外的一篇文章 讲得挺合理,其实该场景并不复杂,由于节点化线程入队后会在 acquireQueued
方法入队,如果入队后发现其前继节点不是哨兵节点,则会阻塞,当某个时刻它被唤醒,此时前继节点便是 head,说明它是第一个非哨兵节点,便会调用 tryAcquire
,进入 hasQueuedPredecessors
方法。
此时进入hasQueuedPredecessors
中 h != t && ((s = h.next) != null && s.thread == Thread.currentThread())
便会发现当前节点满足该条件,然后 CAS
便会成功。
所以我的理解是,s.thread == Thread.currentThread()
这个判断就是针对第一个非哨兵节点的,如果它被唤醒,那么它就不用再排队了,毕竟排了半天才轮到它,怎么可能还需要继续排队呢,所以它就能拿到锁,至于后面的是否入队的问题就更加不存在了。