AQS源码解析(四)

Condition条件锁实现(一)

同步队列VS条件队列

同步队列

在这里插入图片描述
同步阻塞队列(同步队列)是一个双向链表,我们是用prev、next属性来串联节点。Node中还有一个nextWaiter属性,即使在共享锁模式下,这一属性也只为一个标记,指向了一个空节点,因此,在同步队列中,我们不会用它来串联节点。

条件队列

每创建一个Condition对象就会对应一个条件队列,每一个调用了Condition对象的await()方法的线程都会被包装成Node放入一个条件队列中,如下:
在这里插入图片描述
可见,每一个Condition对象对应一个条件队列,每个条件队列都是独立的,互相不影响的。在上图中,如果我们对当前线程调用了condition1.await(),则当前线程就会被包装成Node加到condition1队列的队尾。
值得注意的是,条件队列是一个单项链表,在该链表中我们是用nextWaiter属性来串联链表。但是,就像在同步队列中不会使用nextWaiter属性来串联链表一样,在条件队列中,也并不会用到prev,next属性,它们值都是null。也就是说,在条件队列中,Node节点真正真正用到的属性只有三个:

  1. thread:代表当前正在等待某个条件的线程
  2. waitStatus:条件的等待状态
  3. nextWaiter:指向条件队列中的下一个节点

waitStatus

volatile int waitStatus;
//此线程已经取消
static final int CANCELLED =  1;
//其表示当前node的后继节点对应的线程需要被唤醒
static final int SIGNAL    = -1;
//线程在等待condition条件
static final int CONDITION = -2;
//共享模式下node有可能处于这种状态,表示锁的下一次获取可以无条件传播
static final int PROPAGATE = -3;

在条件列表中,我们只需要关注一个值即可——CONDITION。它表示线程处于条件队列的等待状态,而只要waitStatus不是CONDITION,我们就认为线程不再等待了,此时就要从条件队列中出队。

同步队列和条件队列的联系

一般情况下,同步阻塞的同步队列和条件队列是互相独立的,彼此之间并没有任何关系。但是,当我们调用某个条件队列的signal()方法时,会将某个或所有等待在这个条件队列中的线程唤醒,被唤醒的线程和普通线程一样需要去争锁,如果没有抢到,则同样要被加到等待锁的同步队列中去,此时节点就从条件队列中被转移到同步队列中。
在这里插入图片描述
但是,这里尤其要注意的是,node节点是一个一个转移过去的,哪怕我们是调用的signalAll()方法,node节点也是一个一个转移过去的,而不是整个条件队列接在同步队列的末尾。
同时要注意的是,我们在同步队列中只使用prev、next来串联链表,而不使用nextWaiter;我们在条件队列中只使用nextWaiter来串联链表,而不使用prev、next。事实上,它们它们就是使用了同样的Node数据结构的完全独立的两种链表。因此,将节点从条件队列中转移到同步队列中时,我们需要断开原来的链接(nextWaiter),建立新的链接(prev、next),这某种程度上也是需要将节点一个一个转移过去的原因之一。

入队时和出队时的锁状态

同步队列是等待锁的队列,当一个线程被包装成Node加到该队列中时,必然是没有获取到锁;当处于该队列中的节点获取到了锁,它将从该队列中移除(事实上移除操作是将获取到锁的节点设为新的dummy head,并将thread属性置为null)。

/**
*节点加入队列后,尝试在队列中自旋获取资源
*/
final boolean acquireQueued(final Node node, int arg) {
    //标记是否成功拿到资源
    boolean failed = true;
    try {
        //标记是否被中断
        boolean interrupted = false;
        //自旋
        for (;;) {
            //node的前驱节点,会抛出NullPointerException异常
            final Node p = node.predecessor();
            //如果node的前驱节点是队列头节点head
            //head节点释放资源后
            //紧接着就应该是node尝试获取资源
            //因此调用tryAcquire()尝试获取资源
            if (p == head && tryAcquire(arg)) {
                //如果获取资源成功
                //设置node为新的头结点
                setHead(node);
                //设置原头节点的后继节点next为null
                //帮助垃圾收集
                p.next = null;
                //标记已经获取到资源
                failed = false;
                //返回interrupted=false
                return interrupted;
            }
            //如果node的前驱节点不是队列头节点head
            //或者node节点尝试获取资源失败
            //进入此分支
            //1.检查并更新无法获取资源的节点的状态
            //如果线程应该阻塞,则返回true
            //2.阻塞线程,并检查中断状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //如果1、2条件都满足,需要设置中断标识为true
                interrupted = true;
        }
    } finally {
        //如果node前驱节点为空,抛出NullPointerException异常
        //说明node为头节点,此时failed为true
        if (failed)
            //取消对资源的获取
            cancelAcquire(node);
    }
}
//dummy 节点
private void setHead(AbstractQueuedSynchronizer.Node node) {
    this.head = node;
    node.thread = null;
    node.prev = null;
}

条件队列是等待在特定条件下的队列,因为调用await()方法时,必须是已经获得了lock锁,所以在进入条件队列前线程必然是已经获取了锁;在被包装成Node扔进条件队列后,线程将释放锁,然后挂起;当处于该队列中的线程被signal()方法唤醒后,由于队列中的节点在之前挂起的时候已经释放了锁,所以必须先去再去竞争锁,因此,该节点会被添加到同步队列中。因此,条件队列在出队时,线程并不持有锁。
所以事实上,这两个队列的锁状态正好相反:

  • 条件队列:入队前已经持有了锁 ->在队列中释放锁 ->离开队列时没有锁 ->转移到同步队列
  • 同步队列:入队前没有锁 ->在队列中争锁 ->离开队列时获得了锁

ConditionObject源码解析

AQS对Condition这个接口的实现主要是ConditionObject,它的核心实现就是一个条件队列,每一个在某个condition上等待的线程都会被封装成Node对象扔进这个条件队列。
ConditionObject类实现了Condition接口,ConditionObject中核心属性源码如下:

//条件队列队头节点
private transient AbstractQueuedSynchronizer.Node firstWaiter;
//条件队列队尾节点
private transient AbstractQueuedSynchronizer.Node lastWaiter;

这两个属性分别代表了条件队列的队头节点和队尾节点,每当我们新建一个ConditionObject对象,都会对应一个条件队列。
构造器:

public ConditionObject() {
}

这是一个无参构造方法,只有在真正用到的时候才会初始化条件队列。

await()源码解析

public final void await() throws InterruptedException {
    //如果当前线程在调用await()方法前被中断了,则直接抛出InterruptedException
    if (Thread.interrupted()) {
        throw new InterruptedException();
    } else {
        //将当前线程封装成Node添加到条件队列
        AbstractQueuedSynchronizer.Node node = this.addConditionWaiter();
        //释放当前线程封装Node添加到条件队列
        int savedState = AbstractQueuedSynchronizer.this.fullyRelease(node);
        int interruptMode = 0;
        //如果当前队列不在同步队列中,说明刚刚被await,还没有人调用signal()方法,则直接将当前线程阻塞
        while(!AbstractQueuedSynchronizer.this.isOnSyncQueue(node)) {
            //线程将在这里被阻塞,停止运行
            LockSupport.park(this);
            //能执行到这里说明要么是signal()方法被调用了,要么是线程被中断了
            //所以检查下线程被唤醒的原因,如果是因为中断被唤醒,则跳出while循环
            if ((interruptMode = this.checkInterruptWhileWaiting(node)) != 0) {
                break;
            }
        }
//       if (AbstractQueuedSynchronizer.this.acquireQueued(node, //savedState) && interruptMode != -1) {
//            interruptMode = 1;
//        }
//        if (node.nextWaiter != null) {
//            this.unlinkCancelledWaiters();
//        }
//        if (interruptMode != 0) {
//            this.reportInterruptAfterWait(interruptMode);
//       }
//  }
}

上述代码中的注释描述了大致流程,加下来是await()方法所调用几个方法的具体实现

addConditionWaiter()方法源码解析

await()首先是将当前线程封装成Node扔进条件队列中,调用的是addConditionWaiter()方法。

private AbstractQueuedSynchronizer.Node addConditionWaiter() {
    //检查当前线程是否是获取锁的线程
    if (!AbstractQueuedSynchronizer.this.isHeldExclusively()) {
        throw new IllegalMonitorStateException();
    } else {
        //获取队尾节点
        AbstractQueuedSynchronizer.Node t = this.lastWaiter;
        //如果队尾节点不等于空并且状态不等于CONDITION,清除所有此状态的节点
        if (t != null && t.waitStatus != -2) {
            //清除那些已经取消等待的线程
            this.unlinkCancelledWaiters();
            t = this.lastWaiter;
        }
         //将当前线程包装成Node放入条件队列,传入参数是条件状态-2
        AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(-2);
        //如果对尾节点为空,说明此时队列为空,设置node为头节点
        if (t == null) {
            this.firstWaiter = node;
        } else {
            //如果队尾节点为空,设置队尾节点的nextWaiter指向node节点
            t.nextWaiter = node;
        }
        //设置node节点为尾节点
        this.lastWaiter = node;
        return node;
    }
}

条件队列中是不存在两个线程同时入队的情况的,因为能调用await()方法的线程必然是已经持有了锁,而持有锁的线程只能有一个,所以节点入条件队列不存在并发,不需要CAS操作。
这个方法中,我们就是简单的将当前线程封装成node节点加入到条件队列的队尾。这和讲一个线程封装成node节点加入到同步地列略有不同:

  1. 节点加入同步队列是waitStatus的值为0,而节点在加入条件队列是waitStatus的值为Node.CONDITION。
  2. 同步队列如果队列为空,则会先创建一个dummy node,再创建一个代表当前节点的node添加到dummy node节点的后面;而条件队列没有dummy节点,初始化时,直接firstWaiter和lastWaiter直接指向新建的节点就行了。
  3. 同步队列是一个双向队列,在节点入队后,要同时修改当前节点的前驱和前驱节点的后继;而在条件队列中,我们只修改前驱节点的nextWaiter,也就是说,条件队列是作为单向队列来使用的。

unlinkCancelledWaiters()方法源码解析

如果入队时发现尾节点已经取消等待了,那么node节点就不应该接在它的后面,此时需要调用unlinkCancelledWaiters()来剔除那些已经取消等待的线程。

private void unlinkCancelledWaiters() {
    //获取头节点
    AbstractQueuedSynchronizer.Node t = this.firstWaiter;
    //创建一空的新的节点
    AbstractQueuedSynchronizer.Node next;
    //当头节点不为空时
    for(AbstractQueuedSynchronizer.Node trail = null; t != null; t = next) {
        //获取头节点的下一个节点
        next = t.nextWaiter;
        //判断头节点状态是否为CONDITION(-2)
        if (t.waitStatus != -2) {
            t.nextWaiter = null;
            if (trail == null) {
                this.firstWaiter = next;
            } else {
                trail.nextWaiter = next;
            }

            if (next == null) {
                this.lastWaiter = trail;
            }
        } else {
            trail = t;
        }
    }

}

该方法从头节点开始遍历整个队列,剔除其中waitStatus不为Node.CONDITION的节点,这里使用了两个指针firstWaiter和trail来分别记录第一个和最后一个waitStatus不为Node.CONDITION的节点。

fullyRelease()方法源码解析

在节点成功添加到队列的末尾后,我们将调用fullyRelease()来释放当前线程所占用的锁。

final int fullyRelease(AbstractQueuedSynchronizer.Node node) {
    try {
        int savedState = this.getState();
        if (this.release(savedState)) {
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } catch (Throwable var3) {
        node.waitStatus = 1;
        throw var3;
    }
}

当我们调用这个方法时,说明当前线程已经被封装成Node进入条件队列了。在该方法中我们通过release()方法释放锁。
fullyRelease()方法一次性释放了所有的锁,即对于重入锁而言,无论重入了几次,这里是一次性释放完的。
在这里release(savedState)方法是有可能抛出IllegalMonitorStateException的,这是因为当前线程可能并不是持有锁的线程。虽说只有持有锁的线程才能调用await()方法,但是在调用await()时,我们其实并没有检测getExclusiveOwnerThread() == Thread.currentThread(),换句话说,也就是执行到fullyRelease()这一步,我们才会检测这一点,而这一点检测是由AQS子类实现tryRelease()方法来保证的。

protected final boolean tryRelease(int releases) {
    int c = this.getState() - releases;
    if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    } else {
        boolean free = false;
        if (c == 0) {
            free = true;
            this.setExclusiveOwnerThread((Thread)null);
        }

        this.setState(c);
        return free;
    }
}

当发现当前线程不是持有锁的线程时,就会报错被catch,进入catch块,将当前node的状态设为Node.CANCELLED这就是为什么addConditionWaiter()添加新节点前都会检查尾节点是否已经被取消了。
在当前线程的锁被完全释放了之后,我们就可以调用LockSupport.park(this)把当前线程挂起,等待被signal()方法唤醒。但是,在挂起当前线程之前先用isOnSyncQueued()确保了它不在同步队列中,因为可能其他线程获取到了锁,并发起了signal操作。

final boolean isOnSyncQueue(AbstractQueuedSynchronizer.Node node) {
    //node状态不为CONDITION且前驱节点不为空
    if (node.waitStatus != -2 && node.prev != null) {
        //如果节点的next引用不为空,则肯定在同步队列,否则从队尾向前搜索
        return node.next != null ? true : this.findNodeFromTail(node);
    } else {
        return false;
    }
}

private boolean findNodeFromTail(AbstractQueuedSynchronizer.Node node) {
    //获取队尾节点,队尾节点不等于node节点,队尾节点置为它的前驱节点
    for(AbstractQueuedSynchronizer.Node p = this.tail; p != node; p = p.prev) {
        //tail为空,则不是同步队列而是条件队列
        if (p == null) {
            return false;
        }
    }
    return true;
}

signalAll()方法源码解析

在看signalAll()方法之前,我们首先要区分调用signalAll()方法的线程与signalAll()要唤醒的线程(等待在对应的条件队列里的线程):

  • 调用signalAll()方法的线程本身是已经持有了锁的,现在准备释放锁了;
  • 在条件队列里的线程是已经在对应的条件上挂起了,等待着被signal唤醒,然后去竞争锁

首先,与调用notify()线程必须是已经持有了监视器锁类似,在调用Condition的signal()方法时,线程也是必须已经持有了lock锁:

public final void signalAll() {
    if (!AbstractQueuedSynchronizer.this.isHeldExclusively()) {
        throw new IllegalMonitorStateException();
    } else {
        AbstractQueuedSynchronizer.Node first = this.firstWaiter;
        if (first != null) {
            this.doSignalAll(first);
        }
    }
}

该方法首先校验当前调用signal()方法的线程是不是持有锁的线程,这是通过isHeldExclusively()方法来实现的,该方法由继承AQS的子类来实现,还拿ReentrantLock为例:

protected final boolean isHeldExclusively() {
    return this.getExclusiveOwnerThread() == Thread.currentThread();
}

因为exclusiveOwnerThread保存了当前持有锁的线程,这里只要检查它是不是等于当前线程就行了。
接下来通过firstWater是否为空判断条件队列是否为空,如果条件队列不为空,则调用doSignalAll()方法:

private void doSignalAll(AbstractQueuedSynchronizer.Node first) {
    this.lastWaiter = this.firstWaiter = null;
    AbstractQueuedSynchronizer.Node next;
    do {
        next = first.nextWaiter;
        first.nextWaiter = null;
        AbstractQueuedSynchronizer.this.transferForSignal(first);
        first = next;
    } while(next != null);
}

首先我们通过this.lastWaiter = this.firstWaiter = null;将整个条件队列清空,然后通过一个do-while循环,将原先条件队列里面的节点一个一个拿出来,再通过transferForSignal()方法一个一个添加到同步队列的队尾:

final boolean transferForSignal(AbstractQueuedSynchronizer.Node node) {
    //如果该节点在调用signal方法前已经被取消了,则直接跳过该节点
    if (!node.compareAndSetWaitStatus(-2, 0)) {
        return false;
    } else {
        //如果该节点在条件队列中正常等待,则利用enq方法将该节点添加到同步队列的尾部
        AbstractQueuedSynchronizer.Node p = this.enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !p.compareAndSetWaitStatus(ws, -1)) {
            LockSupport.unpark(node.thread);
        }
        return true;
    }
}

transferForSignal()方法中,我们先试用CAS操作将当前节点的waitStatus状态由CONDITION设为0,如果修改不成功,则说明了该节点已经被CANCEL了,则我们直接返回,操作下一个节点;如果修改成功,则说明我们已经将该节点从等待的条件队列中成功“唤醒”了,但此时该节点对应的线程并没有真正的唤醒,它还要和普通的线程一样去争锁,因此它将被添加到同步队列的末尾等待获取锁。
我们这里通过enq()方法将该节点添加到同步队列的末尾,enq()方法将node节点添加进队列时,返回的是node节点的前驱节点。
在将节点成功添进同步队列中后,我们得到了该节点在同步队列中的前驱节点,在syncqueued中的节点都要靠前驱节点去唤醒,所以,这里要做的就是将前驱节点的waitStatus设为Node.SIGNAL。

signalAll()总结

  1. 讲条件队列清空(只是令this.lastWaiter = this.firstWaiter = null,队列中的节点和连接关系仍然还存在)
  2. 将条件队列中的头节点取出,使之成为孤立节点(nextWaiter,prev,next属性都为null)
  3. 如果该节点处于被CANCELLED了的状态,则直接跳过该节点(由于是孤立节点,则会被GC回收)
  4. 如果该节点处于正常状态,则通过enq()方法将它添加到同步队列的末尾
  5. 判断是否需要将该节点唤醒(包括设置该节点的前驱节点为SIGNAL),如有必要,直接唤醒该节点
  6. 重复2-5,直到整个条件队列中的节点都被处理完

signal()方法源码解析

与signalAll()方法不同,signal()方法只会唤醒一个节点,对于AQS实现来说,就是唤醒条件队列中第一个没有被Cancelled的节点。

public final void signal() {
    if (!AbstractQueuedSynchronizer.this.isHeldExclusively()) {
        throw new IllegalMonitorStateException();
    } else {
        AbstractQueuedSynchronizer.Node first = this.firstWaiter;
        if (first != null) {
            this.doSignal(first);
        }
    }
}

首先检查调用signal()方法的线程是不是已经持有了锁,然后调用doSignal()方法:

private void doSignal(AbstractQueuedSynchronizer.Node first) {
    do {
        if ((this.firstWaiter = first.nextWaiter) == null) {
            this.lastWaiter = null;
        }
        first.nextWaiter = null;
    } while(!AbstractQueuedSynchronizer.this.transferForSignal(first) && (first = this.firstWaiter) != null);
}

这个方法也是一个do-while循环,目的是遍历整个条件队列,找到第一个没有被Cancelled的节点,并将它添加到同步队列的末尾。如果条件队列里面已经没有节点了,则条件队列清空(this.lastWaiter = this.firstWaiter = null)。
在这里用的依然是transferForSignal()方法,但是用到了它的返回值,只要节点被成功添加到同步队列中,transferForSignal()就会返回true,此时while循环的条件就不满足了,整个方法就结束了,即调用signal()方法,只会唤醒一个线程。

signal()总结

调用signal()方法会从当前条件队列中取出第一个没有被Cancelled的节点添加到syncqueue队列的末尾。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值