ReentrantLock 源码简单分析
大部分内容来自转载https://www.cnblogs.com/waterystone/p/4920797.html,我只写了state和Condition部分,
目录
只简单分析非公平锁的实现,不包含公平锁。
只简单分析主要流程,不涉及中断和异常。
节点状态
这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量 waitStatus 则表示当前 Node 结点的等待状态,共有5种取值 CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
-
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
-
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
-
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的
signal()
方法后,CONDITION状态的结点将从条件队列转移到同步队列中,等待获取同步锁。 -
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
-
0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
state
state 是 AbstractQueuedSynchronizer(AQS)
的一个 volatile int
变量,代表着线程同步的资源。state == 0
时代表没有线程竞争资源。ReentrantLock
的资源共享模式是独占模式,当 state
为 1
时,代表线程已经获得锁,其他线程申请锁时会阻塞。ReentrantLock
是可重入锁,当线程已经获得锁时再次申请锁,会增加 state 的值,当线程释放锁时,会减少 state 的值,直到 state 的值为 0 时,其他线程才能申请锁。
lock
线程中调用reentrantLock.lock()
方法获取锁。Sync
是 AQS 的抽象子类,FairSync
和 NonfairSync
这两个类都继承了 Sync
类,分别代表公平锁和非公平锁。在lock()
的代码里,通过cas判断是否能直接获得锁,如果是重入锁或者其他线程竞争未释放的锁,走acquire(1)
方法。
public void lock() {
sync.lock();
}
// 非公平锁实现
static final class NonfairSync extends Sync {
//非公平锁的 lock() 方法实际上调用的是这里
final void lock() {
//通过cas设置状态,state==0时代表资源没被锁,通过cas修改并设置当前线程所得锁
if (compareAndSetState(0, 1))
//设置获得锁的是当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//重入锁和其他线程走这里
acquire(1);
}
}
//AQS类
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
acquire
方法实现在 AQS 类。此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入同步队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()
的语义,当然不仅仅只限于lock()
。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()
的源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
函数流程如下:
-
tryAcquire()
尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待); -
addWaiter()
将该线程加入同步队列的尾部,并标记为独占模式; -
acquireQueued()
使线程阻塞在同步队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。 -
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断
selfInterrupt()
,将中断补上。
以下展示非公平锁的tryAcquire
方法实现:
顾名思义,tryAcquire
方法的意思就是尝试获取锁。tryAcquire
是AQS的抽象方法,在NonfairSync
中实现了tryAcquire
方法,并在其中调用父类Sync
中的nonfairTryAcquire
方法。在nonfairTryAcquire
方法中首先用 cas 检查 state 是不是为0,如果为 0,说明还没有线程获得锁,可以直接获取锁,如同lock
方法。如果state不为0,就检查当前申请锁的线程和独占资源的线程是否为同一个线程,如果是则为重入锁,如果不是则返回false,尝试申请锁失败。
//AQS类抽象方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//非公平Sync类实现tryAcquire方法
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//重入锁,检查当前申请锁的线程和独占资源的线程是否为同一个线程
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;
}
}
接下来看addWaiter
方法。此方法用于将当前线程加入到同步队列的队尾,并返回当前线程所在的结点。
private Node addWaiter(Node mode) {
//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
Node node = new Node(Thread.currentThread(), mode);
//尝试快速方式直接放到队尾。
Node pred = tail;
if (pred != null) {
node.prev = pred;
//cas检查pred是否是尾节点,如果是则将node插入作为新的尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//上一步失败则通过enq入队。
enq(node);
return node;
}
enq
方法,此方法用于将node加入队尾。
如果你看过
AtomicInteger.getAndIncrement()
函数源码,那么相信你一眼便看出这段代码的精华。CAS自旋volatile变量,是一种很经典的用法。还不太了解的,自己去百度一下吧。
private Node enq(final Node node) {
//CAS"自旋",直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node()))
tail = head;
} else { //正常流程,放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t; //返回的是 node 的前节点
}
}
}
}
执行addWaiter
方法后,返回封装了当前线程的Node节点,调用acquireQueued
方法让Node节点进入等待状态。
OK,通过
tryAcquire()
和addWaiter()
,该线程获取资源失败,已经被放入同步队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在同步队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过
//又是一个“自旋”!
for (;;) {
final Node p = node.predecessor();//拿到前驱,前一个节点,前节点不能为null
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (p == head && tryAcquire(arg)) {
setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; // 成功获取资源
return interrupted;//返回等待过程中是否被中断过
}
//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。
先看shouldParkAfterFailedAcquire
方法:整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的wait状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
* CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
查看parkAndCheckInterrupt
方法
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
park()
会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:
被
unpark()
被
interrupt()
注意
park(this)
:ReentrantLock.lock()
时,this是ReentrantLock对象,Condition.await()
时,this是Condition对象。(再说一句,如果线程状态转换不熟,可以参考Thread详解)。需要注意的是,
Thread.interrupted()
会清除当前线程的中断标记位。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
小结
OK,看了shouldParkAfterFailedAcquire()
和parkAndCheckInterrupt()
,现在让我们再回到acquireQueued()
,总结下该函数的具体流程:
-
结点进入队尾后,检查状态,找到安全休息点;
-
调用
park()
进入waiting状态,等待unpark()
或interrupt()
唤醒自己; -
被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
acquireQueued()
分析完之后,我们接下来再回到acquire()
!再贴上它的源码吧:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
再来总结下它的流程吧:
-
调用自定义同步器的
tryAcquire()
尝试直接去获取资源,如果成功则直接返回; -
没成功,则
addWaiter()
将该线程加入同步队列的尾部,并标记为独占模式; -
acquireQueued()
使线程在同步队列中休息,有机会时(轮到自己,会被unpark()
)会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。 -
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断
selfInterrupt()
,将中断补上。
由于此函数是重中之重,我再用流程图总结一下:
unlock
线程中调用reentrantLock.unlock()
方法释放锁。其中调用sync.release(1)
,release
方法实现在AQS类里。
public void unlock() {
sync.release(1);
}
//AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;//找到头结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒同步队列里的下一个线程
return true;
}
return false;
}
逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!
接下来查看tryRelease
方法。此方法尝试去释放指定量的资源。下面是tryRelease()的源码:
//AQS
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //彻底释放锁
free = true;
setExclusiveOwnerThread(null);
}
//如果state不等于0说明是可重入锁,未完全释放完毕
setState(c);
return free;
}
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
接下来看unparkSuccessor
方法。此方法用于唤醒同步队列中下一个线程。下面是源码:
//AQS
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
这个函数并不复杂。一句话概括:用unpark()唤醒同步队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()
联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))
的判断(即使p!=head
也没关系,它会再进入shouldParkAfterFailedAcquire()
寻找一个安全点。这里既然s已经是同步队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()
的调整,s也必然会跑到head的next结点,下一次自旋p==head
就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()
也返回了。
小结
release()
是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒同步队列里的其他线程来获取资源。
线程阻塞时,通过LockSupport.park()
让线程进入等待状态,通过LockSupport.unpark()
唤醒等待的线程。
Condition
await
首先来看await
的源码,主要注意几个方法:addConditionWaiter
、fullyRelease
、isOnSyncQueue
、unlinkCancelledWaiters
。不考虑线程中断。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); //将当前线程添加到条件队列
int savedState = fullyRelease(node); //释放锁,重入锁会一次性将资源释放出来
int interruptMode = 0;
while (!isOnSyncQueue(node)) { //判断是不是在同步队列内
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued 唤醒后将尝试获取锁,不在赘述
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); //在条件队列中将 waitStatus != Node.CONDITION 的节点清除
if (interruptMode != 0) //暂时不考虑中断
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter
addConditionWaiter
将当前线程添加到条件队列。首先判断条件队列上是否还有其他节点,如果有就清除条件队列上的那些waitStatus != Node.CONDITION
的节点(因为这些节点可能已经添加到同步队列或者已经取消)。接下来以当前线程新建节点,等待状态为CONDITION,之后将此节点加入条件队列末尾,如果当前条件队列还没有节点,新建节点既是头节点也是尾节点。
private Node addConditionWaiter() {
Node t = lastWaiter;
//如果条件队列上的节点等待状态已经不是CONDITION,那就会将其从条件队列清除出去
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter; //尾节点可能更新,需要重新设置
}
//以当前线程新建节点,等待状态为CONDITION,之后将此节点加入条件队列末尾
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
unlinkCancelledWaiters
如果条件队列上的节点等待状态已经不是CONDITION,那就会将其从条件队列清除出去。代码简单,请读者自行分析。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
fullyRelease
释放资源,不在赘述
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 重入锁可能持有多份资源,一次性释放,并返回释放的资源个数
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
//尝试释放锁(资源)
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒同步队列上的后继节点
return true;
}
return false;
}
isOnSyncQueue
该方法判断当前节点是不是在条件队列上。在await
方法中,会根据节点是不是在同步队列里来判断是否需要退出循环,至于何时将节点放入同步队列,在下面分析signalAll
方法时会讲到。
final boolean isOnSyncQueue(Node node) {
//如果等待状态是CONDITION或者在同步队列上的前置节点为null
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // 如果同步队列上的后置节点不为null
return true;
return findNodeFromTail(node);
}
//如果节点位于同步队列上,则通过从尾部向后搜索返回true。仅在isOnSyncQueue需要时调用。
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
signalAll
先上源码。isHeldExclusively
简单,主要是需要知道doSignalAll
是干什么的。
public final void signalAll() {
//判断当前线程是否持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
//重写
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
doSignalAll
doSignalAll的源码注释是Removes and transfers all nodes.删除并传输所有节点。删除条件队列上的所有节点并添加到同步队列上。
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first); //添加到同步队列
first = next;
} while (first != null);
}
transferForSignal
首先将节点的等待状态从CONDITION设置为0,如果失败就返回(cas,如果预期等待状态不为CONDITION会失败)。之后调用enq
方法将节点添加进同步队列并返回节点在同步队列的前置节点。判断前置节点的等待状态,如果是CANCELLED状态会直接唤醒,不通过同步队列唤醒,如果不是CANCELLED状态,会尝试将前置节点的等待状态通过CAS设置成SIGNAL,如果失败也会直接唤醒。(直接唤醒应该属于意外情况,我是菜鸡不能理解直接唤醒的意义,所以我将代码复制过来时也将注释一起复制了)。signalAll
是一次性唤醒条件队列上的节点,也就是将多个条件队列的节点转移到同步队列。加入到同步队列的队尾时,只要前置节点不是已经取消调度,就会将前置节点的等待状态设置为SIGNAL,以便在未来唤醒自己。
/**
* Transfers a node from a condition queue onto sync queue.
* Returns true if successful.
* @param node the node
* @return true if successfully transferred (else the node was
* cancelled before signal)
*/
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
回顾 await 并小结
在await
方法中,通过判断节点是否在同步队列来选择是否退出循环。调用signalAll
之后,会将条件队列的节点加入同步队列,等待同步队列调度唤醒。唤醒时节点已经成为同步队列的头节点,自然在同步队列内,以此跳出循环。
while (!isOnSyncQueue(node)) {
LockSupport.park(this); //继续等待
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
简单总结Condition的主要流程:
-
调用await
-
将当前线程封装成节点并添加进条件队列
-
释放锁
注意:释放锁的时候会调用
unparkSuccessor
方法,唤醒同步队列的第二个节点,此节点或得锁后会将自己变成同步队列的头节点 -
进入等待状态
-
调用signalAll
-
将条件队列的节点转移到同步队列
-
等待同步队列调度唤醒