ReentranLock和AQS的关系
在深入分析ReentranLock和AQS之前,我们首先来理清楚一下它们的关系,AQS全名AbstractQueuedSynchronizer队列同步器,是一个能向外提供同步状态(锁)管理的基础框架,ReentranLock正是借助了它从而具备了"锁"的能力。
那么AQS是如何能做到对同步状态进行管理呢?简要来说,它有一个表示同步状态的int变量和一个队列,这个队列用来存放获取同步状态失败的线程,当线程获取同步状态失败之后,当前线程以及等待状态等信息就会被构造成为一个节点,被压进队列中,当同步状态释放时,首节点会把它的后继节点线程唤醒,使其再次尝试获取同步状态。
好像说到这么多,还是不够直观地说明它们之间地关系…接下来我们就先来看看源代码中它们之间是个什么关系吧
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {
sync.lock();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
//此处暂先省略
}
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//此处暂先省略
}
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
//此处暂先省略
}
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
通过源代码我们可以看到,在ReentranLock类中,有一个Sync类,它继承了AQS(简写)类,同时还有两个类继承了Sync类,根据类名我们可以猜出来,一个是公平锁,一个是非公平锁,分别对Sync有进一步的实现,可以看到lock方法在两个子类中被重写,而我们平时看到的Reentranlock调用的lock方法实际上是调用了Sync的lock方法,那么调用的lock方法到底是属于Sync哪个子类呢?通过构造方法我们可以看到默认情况下为非公平锁,当然我们也可以手动指定实现方式为公平锁。ReentranLock和AQS之间的关系也就说清楚啦
既然ReentranLock有两种实现方式,我们不妨先从非公平锁分析起
非公平锁源码分析
为了使分析地更有条理性,我们从ReentranLock调用lock方法开始说起,一步步深入。既然是非公平锁,那么ReentranLock实际调用的就是NonfairSync 类中的lock方法了(多态大家肯定已经知道了吧),我们来看看这个方法里干了啥
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
可以看到,首先是一个CAS操作,从compareAndSetState这个方法名我们可以猜出来这就是在获取同步状态了(大家还记得前面说的AQS中的int值吗,这里操作的就是那个int值了),如果值为0,就把它设置为1,即抢到锁(我们也不说同步状态了,下面都说锁吧),接下来则把当前线程设置为锁的独占线程,抢锁成功,但是实际情况下,成功的可能性可太低了,一般都会进入acquire方法,那么acquire方法是个啥呢?我们不妨看看,我从AQS类中找到了它的具体实现
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在这个方法里,我首先来解释一下它到底干了啥,通俗点说,它会继续抢锁,如果还是没抢到,就会被构造成一个节点,加入到同步队列中。可以看到,它首先会调用tryAcquire进行尝试抢锁的操作,刚刚说了嘛,它没抢到,于是不甘心到这里继续抢,如果抢到了,则tryAcquire返回true,接下来什么都不做,但是很遗憾,相比成功,失败更容易青睐它,抢锁失败返回false之后,它不得不又执行后面的入队操作(入队慢慢排队就还有机会,不入队就真的没机会了…)
好,那我们再来看看tryAcquire里它又是如何抢锁的:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
这个方法最开始是被AQS创建的,它太懒了,什么都没做,而到具体实现,留给了Sync两个子类,既然我们分析的是非公平锁,就先看看非公平苏是如何实现它的吧
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
从非公平锁NonfairSync中找到了这个方法,但是它也还是什么都没做,直接返回了它爹Sync方法的返回值,我们不妨去它爹那里看看nonfairTryAcquire里干了啥…
final boolean nonfairTryAcquire(int acquires) {
//首先,获取当前线程对象
final Thread current = Thread.currentThread();
//判断锁的状态
int c = getState();
//如果c等于0,也就是说锁是无主的
if (c == 0) {
//再调用之前的CAS方法再抢一次,如果抢到了,就把自己设置为锁的独占线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果不为0,也就是说锁是无主的
//再继续判断它自己是不是这个锁的独占线程(说出来你们可能不信,它自己都不知道自己有没有获取这把锁)
//它自己虽然不知道,但是大家到这里可能想到了...这就是可重入锁吧
else if (current == getExclusiveOwnerThread()) {
//如果它真的已经获得过这把锁了,那么就更新锁的状态,实际上就是加个1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果锁有主,但不是自己
return false;
}
可以看到,这个方法里还是有点东西,首先还是看这把锁是不是有主了,如果无主,则还是抢一次,如果有主,则看看是不是自己,如果是自己,就把锁的状态更新,大家应该可以猜到,最后锁是怎么释放的了吧,每次减一,直到为0释放。
我们假设它还是没有这么幸运,因此我们假设这里返回了false,再到acquire方法里
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
返回false之后,接下来就是先执行addWaiter方法,我从AQS这个老祖宗里找到了它,不妨来看看
private Node addWaiter(Node mode) {
//我们都知道,它两次获取锁都失败了,因此还是逃脱不了变成一个节点被插入队列的命运
Node node = new Node(Thread.currentThread(), mode);
//获取到尾节点
Node pred = tail;
//如果尾节点不为空
if (pred != null) {
//把这个新节点的前驱节点设置成尾节点,也就是说把新节点连在尾节点后面
node.prev = pred;
//利用CAS设置尾节点的下一个节点,因为节点太多了,谁都想先入队啊,于是为了保证顺序,采用了CAS
//CAS首先判断这个pred节点是不是尾节点,如果是,就把这个新node变成尾节点,如果不是,循环判断
//因为要入队的节点实在太多了,尾节点一直在变,只好循环更新尾节点
if (compareAndSetTail(pred, node)) {
//再把刚刚的尾节点的下一个节点设置成这个新node,入队成功
pred.next = node;
//返回这个入队的新节点
return node;
}
}
//如果尾节点为空
enq(node);
return node;
}
可以看到上面这个方法里,首先判断队列的尾节点是不是存在,如果存在,则入队,如果不存在,说明队列为空,则执行最后的enq方法创建队列首尾节点,我们看看
private Node enq(final Node node) {
//这里为什么要循环呢?因为一次CAS操作可能成功不了呀
for (;;) {
Node t = tail;
//再次判断尾节点是不是为空
if (t == null) {
//可能刚刚为空,到这里CAS判断不为空了,不能操作,只好下一个循环操作
//如果还是为空,初始化首尾节点,但是不能返回,下一个循环
//可以看到,初始化首尾节点是一个new出来的新节点,而不是没抢到锁入队的节点,也就是锁,队列的首节点不表示线程,
//下一个才是
if (compareAndSetHead(new Node()))
tail = head;
} else {
//无论是被其它节点初始化了首尾节点,还是自己,都会走到这一步
//把目前初始化好的尾节点设置为自己的前置节点
node.prev = t;
//再进行刚刚的操作
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
到这里,加入队列的操作就完成了,也就是说acquireQueued(addWaiter(Node.EXCLUSIVE), arg))中的addWaiter返回了新加入的队列节点,接下来把这个节点传入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
//把失败置为false,表示获取成功了
failed = false;
//返回false的interrupted,意为不阻塞
return interrupted;
}
//如果获取失败,或该节点的前一个结点不是首结点,就判断这个结点是不是应该被挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {//如果抛出了异常,则取消这个结点,把它的状态设置为cancel并进行出队操作
if (failed)
cancelAcquire(node);
}
}
这个方法其实就是循环判断这个节点的前一个节点是不是首结点,如果是则再次去尝试获取锁,如果不是获取获取锁失败就考虑是不是要把这个节点挂起,那么我们详细看看它是如何考虑节点是否应该挂起的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
//如果是signal,也就是说当前节点可以挂起,因为它前驱节点状态表明如果它释放锁了会唤醒当前节点,当前节点可以放心挂起
if (ws == Node.SIGNAL)
//返回true之后紧接着就会执行挂起操作
return true;
//如果状态值大于0,说明它的前驱节点被取消获取锁的资格了
if (ws > 0) {
//那么如果这个时候挂起,前驱节点不可能会唤醒它,于是不断往前找状态不是取消的,同时把这些状态为取消的节点出队
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
//如果是其它状态
} else {
//那么就将它的状态设置为signal,而当前结点先不挂起,在acquireQueued方法里下次循环再到这个方法,对前一个结点状态进行判断
//如果还是signal,就挂起
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回不挂起
return false;
}
这个方法其实就是判断当前结点能否挂起,判断的标准就的是前一个结点的状态是否为signal,也就是说当前结点挂起后,前一个
结点在之后会不会唤醒它,如果会,就可以放心挂起,如果不会,就看前一个结点状态是不是cancel,如果是,就不断往前找状态不是
cancel的结点,把这个结点作为新的前一个结点,把那些状态为cancel的结点出队,如果前一个结点状态既不为signal也不为cancel,就先把它设置为signal,下一次循环到这里再判断一次,再挂起,
判断是否应该挂起就分析好了,接下来这个方法就是直接对它进行挂起操作,等前一个结点释放锁之后,就会唤醒他抢锁了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
到这里,获取锁的过程就分析完了,接下,来我们讨论一下如何释放锁的操作,还是先从lock.unlock()操作分析起
public void unlock() {
sync.release(1);
}
可以看到,调用了release方法,我们不妨进入release方法看看
public final boolean release(int arg) {
//尝试释放锁,如果成功了,就唤醒它的后继节点
if (tryRelease(arg)) {
Node h = head;
//判断头节点的状态
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
//成功
return true;
}
return false;
}
上面这个release方法一共做了两件事,首先释放锁,然后唤醒后继节点,我们先来看看它如何释放锁的
AQS中只是申明了这个方法,具体实现在子类Sync内
protected final boolean tryRelease(int releases) {
//获取锁的状态并更新
int c = getState() - releases;
//如果当前线程并不是独占锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果更新后状态为0,那么就可以释放
//如果是重入锁,那么很可能更新之后不为零,得把加上的锁全部释放才行
if (c == 0) {
free = true;
把当前独占锁的线程置为空
setExclusiveOwnerThread(null);
}
//更新锁状态
setState(c);
return free;
}
好了,独占锁的非公平获取锁以及释放锁到这里就都分析完了,接下来我们看看公平获取锁的方式,首先还是从lock方法看起
公平锁源码分析
public void lock() {
sync.lock();
}
到这里和非公平锁还是一样的,但是这个sync对象和之前非公平锁的sync就不一样啦,这个sync调用的是公平锁子类里的lock方法,我们来看看
final void lock() {
acquire(1);
}
不妨和之前非公平锁那个lock方法对比一下,这次没有先进行CAS操作抢锁,而是直接进入acquire方法,我们来看看
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
毫无疑问,调用的都是AQS老祖宗的acquire方法,没什么不同,但是接下来的tryAcquire方法就不一样了,这个方法不知道大家记不记得AQS老祖宗并没有实现它,具体实现是由子类完成的,也就是说公平锁和非公平锁实现的方式不一样,我们来看看公平锁是如何实现的
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//唯一不同的地方
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
可以看到,和非公平锁的实现方式唯一不同的地方在于首先判断这个节点是不是队头节点的后继节点,如果是才进行抢锁操作,如果不是,就不进行抢锁操作
到这里我们可以总结出来,ReentranLock基于AQS实现的公平锁和非公平锁区别就在于如果是新来的线程能不能进行抢锁操作,公平锁答案是不能,必须先入队,而非公平锁可以先抢两次,抢不到再入队
总结
需要说清楚的是AQS不仅能向外提供独占锁的抢占和释放管理,也能提供共享锁的抢占和释放管理,本文先分析ReentranLock基于AQS实现的独占锁,之后还会继续分析共享锁是如何进行管理的~
参考自《Java并发编程的艺术》
有更好见解的欢迎评论区提出来哦,觉得写的不错不妨点个赞~