ReentrantLock非公平锁源码分析
ReentrantLock源码分析
RenntrantLock是一种独占锁,即是:有且仅有一个线程可以获得该锁,若有一个线程获得该锁,其他线程必须等待;
1.先从其中的非公平锁谈起
那就先说明其加锁的整个过程吧,加锁的流程图如下:
具体加锁的整个调用的方法如上图所示,实现的源代码会做详细的说明 说明之前我先对ReentrantLock类的整个结构进行必要的说明,如下图所示:
其中ReentrantLock实现了Lock和Serializable接口,其中Sync、NonfairSync和FairSync是其内部类,其中Sync继承了AbstractQueuedSynchronizer(AQS)类,非公平锁(NonfairSync)和公平锁(fairSync)都继承了Sync类,由于java语言本身的特点,即间接继承了AQS类,此类中有较多的方法,由子类重写,实现非公平锁(NonfairSync)和公平锁(fairSync)的加锁和解锁过程。
加锁实际上其实很简单,如下所示:
ReentrantLock lock = new ReentrantLock(); // 未传入参数默认使用的是非公平锁
lock.lock();
但是,其实际是如何完成加锁的呢?听我细细道来: 正如下图所示,整个函数的调用过程,完成了加锁的整个流程:(允许我把上个图在拿下来,防止回过头去看,多不方便,是吧!!!)
好了,现在进入源码分析:
// lock.lock()调用的方法实际调用了其内部类的Sync的lock()方法,进行加锁
public void lock() {
sync.lock();
}
点开lock()方法,我们可以看到如下所示的一个抽象方法(在Sync类内部):
abstract void lock();
该方法在Sync内部交由其子类非公平锁(NonfairLock)和公平锁(fairLock)重写; 那么我们进入NonfairSync类内部,可以看到如下代码:
final void lock() {
// 首先执行CAS,判断设置state状态是否成功,设置成功,则将当前线程获得独占锁锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else // 负责执行acquire(1)语句,接着向下看
acquire(1);
}
// 该方法有两个作用:
// 1.线程首先会调用tryAcquire()方法尝试获取锁;
// 2.将未获取到锁的线程挂起,等待被唤醒;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其中,tryAcquire(arg)完成作用1,acquireQueued(addWaiter(Node.EXCLUSIVE), arg))完成作用2;
接着进入tryAcquire(arg)源码分析,发现点开一看,如下所示,又调用了一个方法,不怕,继续向下找,总能找到想要看的内容:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire(acquires)源码就是这样:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState(); // 获取state变量的值
if (c == 0) { // 判断state是否为0,若成立,则进入if语句
if (compareAndSetState(0, acquires)) { // 执行CAS操作,设置state变量为1,即:当前线程获得同步状态(锁)
setExclusiveOwnerThread(current); // 将当前线程设置为独占式
return true;
}
}
// 判断当前线程是否是当前正在执行的线程,若是,则执行
// 此处就可以理解为什么ReentrantLock是重入锁了。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 若以上条件都不成立,则返回false,得到此返回的结果,就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,下面会对此分析;
}
好的进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)该方法体内,首先执行的是addWaiter(Node.EXCLUSIVE)方法,源码如下:
private Node addWaiter(Node mode) {
// 首先,将当前线程,封装为Node节点,其Node类是AQS的内部类
Node node = new Node(Thread.currentThread(), mode);
// 指向尾结点
Node pred = tail;
if (pred != null) { // 若尾结点不为空
node.prev = pred; // 获得前驱节点
if (compareAndSetTail(pred, node)) { // 利用CAS将当前插入到尾部
pred.next = node;
return node;
}
}
enq(node); // 第二个if条件不成立,执行此方法,依然是将节点入队
return node; // 返回当前节点,进入acquireQueued()方法
private Node enq(final Node node) {
// 死循环
for (;;) {
Node t = tail; // 获取尾部节点
if (t == null) { // 若尾结点为空
if (compareAndSetHead(new Node())) // 执行CAS常见一个新节点作为头结点,并且创立成功,则将尾结点指向头结点
tail = head;
} else { // 存在尾结点
node.prev = t;
if (compareAndSetTail(t, node)) { // 利用CAS将传入的节点添加到尾部
t.next = node;
return t; // 返回当前节点的前驱节点
}
}
}
}
线程的挂起,以及线程被唤醒后尝试获取锁的代码关键就在acquireQueued()方法中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 死循环
final Node p = node.predecessor(); // 获取前驱节点
if (p == head && tryAcquire(arg)) { // 前驱节点是头结点,尝试获取锁
setHead(node); // 设置当前节点为头结点,其方法内部清除了线程的相关信息
p.next = null; // help GC
failed = false;
return interrupted;
}
// 从字面意思就可以看出,获取锁失败应当挂起线程,当然挂起线程是parkAndCheckInterrupt()方法执行
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
接着在看shouldParkAfterFailedAcquire(p, node)方法与parkAndCheckInterrupt()方法是如何挂起线程的。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获得当前节点的前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 若当前节点为待唤醒状态,则返回true,
return true;
if (ws > 0) { // 若当前状态为CANCELED状态
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 寻找状态为小于0的状态(SIGNAL或CONDITION或PROPAGATE(RentrantLock中暂时不考虑))
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 设置前驱节点为SIGNAL状态
}
return false;
}
parkAndCheckInterrupt()方法如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 挂起当前线程
return Thread.interrupted();
}
至此,整个加锁的过程就结束了,可能有些人比较迷惑,同步队列是如何构造的?解锁后,线程又是如何获取锁的?这些留待下篇文章进行分析^_^。
转载自:https://juejin.cn/post/7037320021361721351