AQS源码学习(二)----- Condition

文章目录

  • 并发与同步:
    • 临界区
    • Spinlock实现
  • 信号量与管程
    • 信号量
    • 管程
  • Java中Condition实现
    • await
      • 完全释放锁fullyRelease
      • 挂起线程
    • signal
      • 转移到CLH
      • 唤醒后检查中断状态
      • 处理中断状态
    • 参考文章

上一篇结合ReentrantLock介绍了独占锁的源码,以及对AQS中相关源码进行了解析,这一篇来看一下Condition。
为了能够讲明白这个东西,这里先上一些操作系统的基本知识,希望大家耐心看完。
这一篇会比较长,希望读者有耐心读完,我参考了多篇博客,结合自己对AQS的理解整理而成,个人感觉理解的还算透彻。

并发与同步:

并发不是多核时代的产物,在早期的多核CPU已经通过时分复用来实现程序之间的并发。并发带来的好处很多,比如资源共享、提高程序执行效率、模块化等等。但并发对编程带来了很多挑战,比如互斥、死锁。

所谓同步互斥,就是在并发的情况下,保证一些操作的原子性。原子操作(Atomic Operation)即在一次执行过程中要么全部成功,要么全部失败的操作,不存在部分执行的情况。原子性在各种应用场景都非常重要,比如数据库的ACID,其中A就是原子性。生活中,也要很多原子性的例子,比如银行卡之间转账,分成两个操作:A扣钱,B加钱,但这两个操作必须满足原子性,不然就出大问题了。

我们常见意义上的并发一般是指多进程之间或者多线程之间。当然,近些年越来越流行的协程也算一种并发。对于非操作系统内核开发人员,需要同步互斥的场景大多数都是线程之间的并发。更好的资源共享是线程的特性之一,下图(来自UCSD:Principles of Operating Systems )所示是同一个进程内三个线程的内存使用情况:
AQS源码学习(二)----- Condition_第1张图片
对于每一个进程内的多个线程,static data segment(包括全局变量、static对象)、Heap(堆,malloc和new分配的空间)是共享的。每个线程有自己独立的Stack(栈),存储局部变量。

后文主要以多线程为例,但同样适用于多进程。

临界区

提到并发编程,首先就想到临界区(critical section)这个概念,临界区是线程中访问临界资源的一段需要互斥执行的代码。临界资源是指线程之间共享的资源,但不同的执行序列结果不确定的,这也叫做竞态条件(race condition)。举个例子,我们都知道linux的系统调用fork会创建一个子进程,该调用在父进程会返回子进程的PID,操作系统中每个进程的PID必须是独一无二的。假设pid的生成是这样的:

new_pid = next_pid++

上面的代码中next_pid就是临界资源,多个进程可能同时访问这个资源, 但上述代码不是原子的(在汇编下是几条指令),但是需要互斥执行的,因此需要临界区。当然,临界区只是一个概念,具体怎么实现依赖于系统与编程语言。在编码中使用临界区的伪代码如下:

entry section

critical section

exit section

分为三部分:

entry section: 判断能否进入,如果能则设置标志,不能则等待

critical section:需要互斥访问的代码段

exit section: 清除设置的标志,使得其他进程(线程)可以进入临界区

临界区的实现有几种方式:

  • 禁用硬件中断

我们知道,系统调用以及执行流程的切换都是依靠软中断。禁用中断之后,进程(线程)就不会被切换出去,从而保证代码段能执行结束。但坏处也很明显,由于中断被禁用,如果临界区代码一直执行,其他进程就没机会执行了。而且,只能禁止单个CPU的中断。

  • 基于软件同步

即基于代码实现同步互斥,比较有名的是peterson算法,用来解决两个进程对临界区的互斥访问问题。

  • 基于原子操作原语的方法

上述两种方式都比较复杂,我们需要更加高级的武器。支持并发的语言都提供了锁(lock)这个概念,在现实生活中也很好理解,如果只能一个人在屋子里,那么进去之后就锁上,出来的时候再打开锁;没有锁的人只能在外面等着。在编程语言中,大概是这样子的:
  > acquire(lock)

critical section

release(lock)

acquire,release实现的也就是entry section和 exit section的功能,上面的代码是面向过程的写法,面向对象一般写成lock.acquire和lock.release,或者使用RALL(Resource Acquisition Is Initialization)来避免release函数未被调用到的问题。

lock的实现需要基于硬件提供的“原子操作指令”,这些操作虽然理解起来是几步操作,但硬件保证其原子性。比较常见的原子操作指令包括 test and set、compare and swap,接下来通过test and set来看看lock的实现。

也就是说,cas操作其实是操作系统原语,通过cpu的lock cmpxchg指令完成

而mutex和条件变量其实是操作系统提供的一种机制来保证互斥。

Spinlock实现

test-and-set是一个原子操作,其作用对某个变量赋值为1(set),并返回变量之前的值,下面用C语言描述这个过程

1     #define LOCKED 1
2     int TestAndSet(int* lockPtr) {
3         int oldValue;
4          
5         oldValue = *lockPtr;
6         *lockPtr = LOCKED;
7 
8         return oldValue;
9     

对应的acqurie和release的伪码如下:

void acquire(int *lock){
    while(TestAndSet(*lock));
}

void release(int *lock){
    *lock = 0;
}

在acquire函数中,如果TestAndSet返回1,那么while循环就一直执行(也就是在这里等待),直到另一个线程调用release。当然,这个实现看起来不太好,主要是等待的线程会不停的检查,浪费CPU,这个问题称之为忙等待(busy-wait or spin-wait),所以这个lock的实现也叫自旋锁spinlock。解决办法是如果需要等待,那么该线程主动交出执行权,让其他线程有机会执行,这种方式称之为让权等待(yield-wait or sleep-wait),应用开发人员使用的互斥锁一都是指这种情况。

其实就是想办法,在cas失败之后,避免无限的自旋,而是让出cpu

信号量与管程

​ 在上一部分介绍了并发、同步互斥、临界区等基本概念,也介绍了利用原子操作指令实现锁(lock)的机制与实现。但这个的lock(spinlock)是最基础的同步原语,实际操作系统需要封装更高级的同步原语来实现更复杂、更实用的功能。信号量(semaphore)和管程(monitor)就是操作系统提供的两种更高级的同步方式,在操作系统(linux)和编程语言都有对应的实现和封装,本章节对二者进行介绍和对比。

信号量

semaphore是大牛Dijkstra(没错,就是出现在数据结构或者图论中的Dijkstra)在20世纪60年代发明的概念,用来协调并发程序对共享资源的访问。注意,这里是协调,而不是互斥,协调的概念更广泛一些,也包括并发程序之间的协作。semaphore有一个整型变量(S)和两个原子操作组成S代表资源的数量,而两个原子操作一般被成为P操作和V操作(有时也被成为wait、signal)

P操作表示申请一个资源,P操作的定义:S=S-1,若S>=0,则执行P操作的线程继续执行;若S<0,则置该线程为阻塞状态,并将其插入阻塞队列。伪码如下:

Procedure P(Var S:Semaphore);
  Begin
    S:=S-1;
    If S<0 then w(S) {执行P操作的线程插入等待队列}
  End;

V操作表示释放一个资源,V操作定义:S=S+1,若S>0则执行V操作的线程继续执行;若S<0,则从阻塞状态唤醒一个线程,并将其插入就绪队列。伪代码如下:

Procedure V(Var S:Semaphore);
  Begin
    S:=S+1
    If S<=0 then R(s) {从阻塞队列中唤醒一个线程}
  End;

信号量在现实生活中很容易找到对比的例子,比如银行的窗口数量就是S,在窗口办理业务就是P操作,业务办理结束就是V操作。

根据S初始值的不同,semaphore就有不同的作用。如果S初始值为1,那么这个semaphore就是一个mutex semaphore,效果就是临界区的互斥访问。如果S初始值为0,那么就是用来做条件同步,效果就是必须等待某些条件发生。如果S初始值为N(N一般大于1),那么就是用来限制并发数目,也被称之为counting semaphone

管程

管程是编程语言提供的一种抽象数据结构,用于多线程互斥访问共享资源。首先,是互斥访问,即任一时刻只有一个线程在执行管程代码;第二,正在管程内的线程可以放弃对管程的控制权,等待某些条件发生再继续执行。第二点就厉害了,不管是之前提到的互自旋锁还是信号量,进入了临界区域除非代码执行完,否则是不会出现线程切换的,而管程可以主动放弃执行权,这反映到编码上也会有一些差异。

也就是说:管程不是一种特殊的原语,只是互斥锁和条件变量的配合使用的一种抽象数据结构;

管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。

管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

管程实现对共享资源的互斥访问,一个管程包含:

  • 多个彼此可以交互并共享资源的线程
  • 多个与资源使用有关的变量(互斥量)
  • 一个互斥锁
  • 一个用来避免竞态条件的不变量(while条件中判断的变量)

因此对条件变量存在两种主要操作:

  • wait c 被一个线程调用,以等待断言 P c P_c Pc 被满足后该线程可恢复执行. 线程挂在该条件变量上等待时,不被认为是占用了管程.
  • signal c (有时写作notify c)被一个线程调用,以指出断言 P c P_c Pc现在为真.
    在下述例子中, 用管程实现了一个信号量. 一个私有整型变量s需要被互斥访问。管程中定义了子程序“增加”(V)与子程序“减少”§,整型变量s不能被减少到小于0; 因此子程序“减少”必须等到该整型变量是正数时才可执行. 使用条件变量sIsPositive与相关联的断言 P s I s P o s i t i v e = ( s > 0 ) P_sIsPositive = (s>0) PsIsPositive=(s>0).
monitor class Semaphore
{
  private int s := 0
  invariant s >= 0
  private Condition sIsPositive /* associated with s > 0 */

  public method P()
  {
    if s = 0 then wait sIsPositive
    assert s > 0
    s := s - 1
  }

  public method V()
  {
    s := s + 1
    assert s > 0
    signal sIsPositive
  }
}

当一个通知(signal)发给了一个有线程处于等待中的条件变量,则有至少两个线程将要占用该管程: 发出通知的线程与等待该通知的某个线程. 只能有一个线程占用该管程,因此必须做出选择。两种理论体系导致了两种不同的条件变量的实现:

  • 阻塞式条件变量(Blocking condition variables),把优先级给了被通知的线程.
  • 非阻塞式条件变量(Nonblocking condition variables),把优先级给了发出通知的线程.

看过上面维基百科对管程的介绍,发现上面的概念仿佛Java已经实现了,不错,synchronized与wait、notify 以及Java1.5引入的Lock、Condition,就是对管程相关实现。找到了Condition的理论基础,相信我们对Condition的用途与实现方式有个初步的认识。下面我们就用Lock与Condition结合实现的生产者消费者例子来分析Condition的实现原理(看上去java的实现应该是,使用了非阻塞条件变量?)

管程,互斥量,条件变量等,是操作系统提供的原语,不用cpu支持,因为其使用了等待队列等数据结构,应该是封装在内核代码中,通过posix接口堆外暴露,只是一些系统调用;

而cas则是cpu级别的原语

Java中Condition实现

先来看一个使用Condition实现的生产者消费者的例子

public class Buffer {
    private List<String> queue;
    private int size;

    private final Lock lock = new ReentrantLock();
    // 读线程条件(消费者线程 条件队列)
    private final Condition notEmpty = lock.newCondition();
    // 写线程条件(生产者线程 条件队列)
    private final Condition notFull = lock.newCondition();

    public Buffer() {
        this(10);
    }

    public Buffer(int size) {
        this.size = size;
        queue = new ArrayList<>();
    }

    public void put() throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == size) {
                System.out.println("[Put] Current thread " + Thread.currentThread().getName() + " is waiting");
              //现在queue已满,所以producer要等待集合 不满这个条件
                notFull.await();
            }

            queue.add("1");
            System.out.println("[Put] Current thread " + Thread.currentThread().getName()
                    + " add 1 item, current count: " + queue.size());
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public void take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == 0) {
                System.out.println("[Take] Current thread " + Thread.currentThread().getName() + " is waiting");
              //现在集合已空,这里等待集合非空条件
                notEmpty.await();
            }

            System.out.println("size = " + queue.size());

            queue.remove(queue.size() - 1);
            System.out.println("[Take] Current thread " + Thread.currentThread().getName()
                    + " remove 1 item, current count: " + queue.size());
            notFull.signal();
        } finally {
            lock.unlock();
        }

    }
}

注意,要明确一个理念:

所有的锁,都是要保证对临界值的访问,这个临界值是业务上的临界值,而不要理解为AQS的state,AQS的state是代表锁获取的次数,是用来帮助控制对临界值访问的;业务代码访问的临界值通常可能是集合,文件,或者field

而一般来说,对临界资源的读是不需要Condition的,因为读不涉及到修改,也就不需要在Condition上wait和notify,因为其不涉及生产和消费;自然也不需要wait一个断言,由于自己不修改数据,也无法signal去修改一个断言去唤醒另一个线程(这里的断言,其实就是业务系统的临界资源,结合java和管程中的概念去理解);这也解释了为什么 ReentrantReadWriteLock中,readLock.newCondition()会抛异常,也就是不支持读锁上使用condition,因为不涉及到并发修改。
在使用Condition之前,当前线程必须获得锁,然后调用lock.newCondition()获取一个Condition对象,这个操作是在AQS中的,ConditionObject是AQS的内部类,下面是该类的一部分代码:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

    /**
     * Creates a new {@code ConditionObject} instance.
     */
    public ConditionObject() { }

    // Internal methods

    ......
}

在维基百科中,我们了解到:一个条件变量就是一个线程队列(queue),所以Condition的实现也应该是基于队列的。从上面代码可以发现,ConditionObject有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

firstWaiterlastWaiter分别是condition queue的第一个和最后一个结点,所以Condition自己维护了一个队列,那这个队列和CLH有没有关系呢?我们下面接着来看。
下图是简单总结了 AQS中CLH和条件队列的关系:
AQS源码学习(二)----- Condition_第2张图片

await

在上面的生产者消费者代码的put方法中,当队列满了,会调用notFull.await()方法,那么这个方法做了什么操作呢?代码如下:

注意:await一定是线程安全的,因为await只工作在独占锁下,而且必须是先lock了才能进入await

        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;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

await方法是响应中断的,首先如果检测到线程中断,就会立即抛出InterruptedException。接着调用addConditionWaiter()方法,代码如下:

/**
 * Adds a new waiter to wait queue.
 * @return its new wait node
 */
private Node addConditionWaiter() {
    if (!isHeldExclusively())				//这里保证了是当前线程获取锁,而且是独占的,也保证了线程安全
        throw new IllegalMonitorStateException();
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
  //如果tail存在并且tail不是 Condition状态,那么就清理当前Condition的队列,重新拿到tail
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }

    Node node = new Node(Thread.currentThread(), Node.CONDITION);

    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

addConditionWaiter()方法中,首先会调用isHeldExclusively()当前线程是否处于独占模式,注意该方法的实现在AQS的子类中;

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();
}

可见Condition是用在独占模式下的,共享模式下会直接报错。接着获取 队列中最后一个节点t = lastWaiter,并且判断t是否为空,并且t的waitStatus是否为Node.CONDITION,根据上面的注释,如果t的waitStatus不是Node.CONDITION,证明该节点已经被取消了,需要将其从队列中移除,会调用unlinkCancelledWaiters()方法,该方法源码如下:

private void unlinkCancelledWaiters() {
    Node t = firstWaiter; // 获取头节点
    Node trail = null;  // 表示当前节点的上一个节点
    while (t != null) { // 遍历队列
        Node next = t.nextWaiter; // 获取后继节点
        // 判断当前t指向的node 的waitStatus是否为 Node.CONDITION
        if (t.waitStatus != Node.CONDITION) { 
            t.nextWaiter = null; // 如果是,则置空当前节点的后继,等待垃圾回收
            if (trail == null) 		//如果前驱是null,则代表第一次循环就进入到t.waitStatus != Node.CONDITION,所以才可能trail==null,否则至少trail会暂存一次t
                firstWaiter = next; // 说明从head开始,一直都处于剔除状态,所以让当前节点的后继节点作为头结点
            else
              	//至少t的上一个节点,在上一次循环中没有被剔除还通过 trail=t保留了下来
                trail.nextWaiter = next; // 由于当前节点不符合要求,需要被踢出队列,只好用上一个节点来链接next节点
            if (next == null) // 判断后继节点是否为空
                // 如果next为空,代表当前节点没有后继节点,注意此时当前节点的waitStatus不等于 Node.CONDITION,
                // 上面的操作已经被置空了,等待被垃圾回收,所以从当前t往后再没有节点了,直接将上一个节点置为tail,当然,这里tail可能被置为null,因为trail也可能为null,这种极端情况就是所有队列节点状态都不是CONDITION
                // 就将上一个waitStatus是否为 Node.CONDITION的结点作为尾节点
                lastWaiter = trail; 
        }
        else
            // 如果当前节点的waitStatus为Node.CONDITION,所以不能剔除,所以要在这将当前节点暂存到trail
          //用于在下一次循环中代表前驱节点
            // 那么将当前节点赋值给trail,相当于暂存一下
            trail = t; 
        t = next; // 继续向后遍历
    }
}

总结来说,unlinkCancelledWaiters()方法是踢出队列中waitStatus不是Node.CONDITION的节点。

这其实是一个单链表的移除逻辑

让我们回到addConditionWaiter()方法,由于在unlinkCancelledWaiters()方法,队列的尾节点会产生变化,所以在执行unlinkCancelledWaiters()方法后,需要再重新获取一下尾节点。接着构造一个新的节点node,注意此时的Condition队列的节点还是用的是CLH中的节点Node类型,这一点想必大家都熟悉了,此时传递一个Node.CONDITION参数,这一步操作是设置节点的waitStatus的值为Node.CONDITION

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }

完全释放锁fullyRelease

让我们回到await()方法中,addConditionWaiter()方法执行完后,获得了一个新创建的节点node,接下来执行fullyRelease(node)方法,参数为刚才新创建的节点,源码如下:

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) { //aqs的release,如果可能的话(如果clh存在,则当前线程很可能是head,或者当前head是一个 dummy head,则head.next是一个等待线程;也就是无论怎样;要么head为null说明不需要唤醒;要么head是一个假的,则说明还没有进行过真正的排队node获取head,则唤醒老二,要么当前持有锁的就是head,还是唤醒老二;所以要么就是不唤醒(head为null),唤醒的话就是唤醒head.next),将当前节点从获得CLH队列中拿走
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

这个方法最终调用了AQS的release(),并且调用当前锁实现的tryRelease();以ReentrantLock为例

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

可以发现,释放过程中可能会抛异常;而fullyRelease方法接收到任何异常,都会把 当前node.waitStatus = Node.CANCELLED;

我们回到fullyRelease方法中,可以看出,对于可重入锁,这个方法会直接导致state的值为0,也就是完全释放锁,返回值为释放锁之前的state的值。

挂起线程

回到await()方法

接着进入一个while循环,循环条件是:!isOnSyncQueue(node),这个是什么意思?看其源码:

        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;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

    final boolean isOnSyncQueue(Node node) {
        // 如果节点的waitStatus是Node.CONDITION,那么说明当前节点在条件队列中
    // 由于条件队列是单链表,那么节点的prev必然是空
      //也就是condition队列没有prev属性,这里就是判断当前这个node位于clh还是条件队列
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
      //    // 如果节点的next有值,我们知道next是CLH中的,条件队列中没有此值,
    // 说明当前节点必然在CLH队列中
        if (node.next != null) // If has successor, it must be on queue
            return true;
        
      //如果前面两个条件都不满足,则从 clh中看看能不能找到一个node等于当前node
        return findNodeFromTail(node);
    }

分析isOnSyncQueue方法,如果节点的waitStatus是Node.CONDITION,那么说明当前节点在条件队列中,由于条件队列是单链表,那么节点的prev必然是空;如果节点的next有值,我们知道next是CLH中的,条件队列中没有此值,说明当前节点必然在CLH队列中。那么还有什么情况没有考虑到呢?就是node.prev != null 并且 node.next == null 的情况,仿佛这种情况就在CLH队列里了,这种情况可能存在,因为在clh中addWaiter()方法,prev赋值在cas之前,prev赋值总能成功,所以只要是要进入clh,则prev!=null,而next由于cas过程可能失败,则可以为null

所以可能存在 prev!=null && next == null,这种情况也能说明node在clh中

所以需要从阻塞队列的队尾往前遍历,如果找到,返回 true,下面是findNodeFromTail代码。

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

这里有个疑问,因为await()方法中,

Node node = addConditionWaiter();

这行代码应该保证了这个node的ws一定是CONDITION?而且这个node是new出来的,不太可能位于clh?

回到await方法中,我们看了isOnSyncQueue方法的实现,isOnSyncQueue(node) 返回 false 的话,就会进入到while循环内部,执行LockSupport.park(this);挂起当前线程。await的代码等我们看了signal()方法再来看。

signal

上面我们分析到在await方法中线程被挂起了,那么这个线程如何被唤醒呢?回到最上面的生产者消费者代码中put方法,当添加一个数据后,我们调用了notEmpty.signal();来通知消费者线程可以消费者数据了,我们可能想象到此时应该唤醒notEmpty条件队列中被阻塞的线程,下面我们来分析下源码。下面是signal()的源码:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;			//唤醒队列的第一个
    if (first != null)
        doSignal(first);
}

signal()方法中,首先判断调用 signal 方法的线程必须持有当前的独占锁,如果不是,直接抛出IllegalMonitorStateException异常。

接着获取条件队列的首节点,不为空的话,就调用doSignal(first)方法,下面是该方法的源码:

        private void doSignal(Node first) {
            do {		//先把firstWaiter 指向原来first.nextWaiter,如果nextWaiter是null,说明条件队列就一个元素
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;				
                first.nextWaiter = null;  		//由于下面要把first给转移到clh了,所以要将first这个节点不引用别的node
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

一旦if中的判断(firstWaiter = first.nextWaiter) == null成立了,那么while也就退出了,因为不满足

(first = firstWaiter) != null,而while中 (first = firstWaiter) != null的意思就是重新获取一次first再次循环;

从上面的代码我们可以发现,方法内部是一个do while循环。在循环体内,首先调用firstWaiter = first.nextWaiter将条件队列的首节点的下一个节点赋值给首节点(firstWaiter),相当于将原来的首节点移除条件队列,然后判断新的首节点是否为空,如果为null,说明此时队列就已经空了,直接把尾节点赋值为空。然后将原来的首节点的nextWaiter赋值为空,等待垃圾回收该节点。

转移到CLH

那么while的循环条件是什么呢?在while循环内,首先会调用transferForSignal(first)方法,注意此时的参数first还是原来的头节点,该方法代码如下:

/**
 * 当返回true时代表成功转移到CLH队列中
 * 返回false代表在 signal 之前,当前节点已经取消了。
 */
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    if (!node.compareAndSetWaitStatus(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);			//node进入clh,并返回clh中的前驱节点
    int ws = p.waitStatus;
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

在transferForSignal方法内,首先利用 CAS 将传入节点的waitStatus由Node.CONDITION设置为0

  • 如果失败,说明当前节点已经被取消了,不需要再转移到CLH队列,直接返回false;
  • 如果成功,接着将调用enq(node)方法,该方法会将传入的节点自旋进入CLH队列的队尾注意,这里的返回值 p 是 node 在阻塞队列的前驱节点(该方法不再进行分析)。(enq中返回的是新插入节点的prev,也就是oldTail节点)

接着获取p的waitStatus,下面会分两步判断:

  1. 当ws大于0时,代表当前节点的prev在CLH队列中已经被取消了(所以可以唤醒当前节点,让其唤醒了来移除clh中的 cancel节点
  2. 如果ws<=0,接着会调用p.compareAndSetWaitStatus(ws, Node.SIGNAL),利用CAS将p的waitStatus由原来的值设置为Node.SIGNAL,在CLH队列中,我们知道,如果当前节点想要被唤醒继续获取锁,那么该节点的前驱节点的waitStatus必须为Node.SIGNAL
  3. 也就是说,node p作为前驱,如果状态正常(ws <=0),如果cas给其设置状态为signal成功,则!p.compareAndSetWaitStatus(ws, Node.SIGNAL)为false,所以整个if不成立(两个或条件均不成立),意思就是前驱节点已经是signal了,那么当前节点没必要唤醒,直接在clh中等待释放锁就行了;也就是说即便在condition中 等待的节点被signal了,那也只是到了clh中继续排队
  4. 如果p.compareAndSetWaitStatus(ws, Node.SIGNAL) 失败,也就是说有并发修改前驱节点的状态,那么很可能是前驱节点cancel了,此时唤醒当前node,让它去做清洁工来清理一下clh中异常的node;

综合上面两步,如果当前节点的前驱节点被取消或者前驱节点CAS设置waitStatus为Node.SIGNAL失败,就会调用LockSupport.unpark(node.thread);唤醒当前节点的线程,唤醒之后如何操作呢?这时我们需要到await方法来看,这个稍后再说。

综合以上,对于transferForSignal方法,我们可以知道:当返回true时代表成功转移到CLH队列中;返回false代表在 signal 之前,当前节点已经取消了

我们回到 doSignal 方法,while循环的条件是:!transferForSignal(first) 并且 (first = firstWaiter) != null,即如果转移节点到clh失败(当前节点已经取消),那么就看看当前向下移动游标,first=firstwaiter(firstWaiter在do代码块中,已经指向了first.nextwaiter),然后相当于判断下一个节点是不是null,如果不是则继续尝试转移下一个节点,否则退出循环

综合doSignal方法,我们可以知道:如果 first 转移至CLH不成功,那么选择 first 后面的第一个节点进行转移,依此类推

唤醒后检查中断状态

这里我们先总结一下,什么情况下LockSupport.park(this);会继续向下执行?

  1. 当节点在signal后转移至CLH队列后,被前驱节点release动作唤醒!
  2. 在CLH队列中,当前节点的前驱节点被取消或者前驱节点的waitStatus由原来的值设置为Node.SIGNALCAS设置失败(所以在transferForSignal方法中直接唤醒了当前线程,也就是被其他线程signal这个动作唤醒

我们回到await方法,在while循环中,当线程获取到锁后,就会接着执行挂起之后的方法:

            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

如上面代码所示,interruptMode代表什么意思呢?我们发现接着执行了checkInterruptWhileWaiting(node)方法,代码如下:

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

从注释来看,该方法用来检测当前节点的线程在wait过程中的中断状态,有三个值:

  • THROW_IE(-1): 在被signalled之前,当前节点的线程被中断了
  • REINTERRUPT(1): 在被signalled之后,当前节点的线程被中断了
  • 0: 未发生中断

在checkInterruptWhileWaiting方法内,首先判断Thread.interrupted()线程中断标识,如果返回true,就需要判断线程是在 signal 之前还是之后中断的,否则返回0。

这里有个问题,怎样判断线程是在signal 之前还是之后中断的?接着就看transferAfterCancelledWait(node)方法,根据返回值可知,返回true, 代表在signal之前被中断了;返回false,代表在signal之后被中断了。源码如下:

final boolean transferAfterCancelledWait(Node node) {
    if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    
    
    //如果是signal后的中断,则判断这个node是否位于clh
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

transferAfterCancelledWait方法内,首先利用CAS将node的waitStatus由Node.CONDITION 设置为0,注意这一步,在上面我们分析signal操作的transferForSignal方法时我们知道:当前节点的线程如果被signal的话,那么它的waitStatus已经由Node.CONDITION设置为0了。所以此时再进行此操作成功,说明该节点的线程在signal之前被中断了,此时调用enq(node)进入CLH队列,返回true;

疑问:

node的线程,从park状态醒来,通常是因为signal、或者signal后的clh中前驱节点release资源而唤醒,但是这都是先执行了signal才导致的;

发生在signal之前的中断可能是在await()方法中,while循环之前、而if (Thread.interrupted())判断之后这段空挡发生的,这就算是signal之前发生的中断

如果由Node.CONDITION设置为0失败,说明signal方法已经设置过了,注意在transferForSignal方法中,设置成功后,还要将当前节点转移到CLH队列中,这个阶段需要时间来完成,所以while (!isOnSyncQueue(node)) 这里判断是否已经进入CLH成功,如果不成功,就让出CPU时间片,等待其他线程将这个node放到clh完成。最后transferAfterCancelledWait返回false。这里注意一点,上面在中断检查时,如果在signal过程中发生了中断,节点依然会进入CLH队列(也就是仍然算作signal之后的终端)

此时我们再次回到await()的循环体内,从上面的分析可以看出,跳出循环的条件是:

  1. signal的转移操作成功,节点已在CLH队列中,while条件不满足
  2. 节点的线程发生了中断,break

紧接着,await的剩余代码:

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;
            }
  			//当前线程醒了(被前驱节点叫醒或者被signal叫醒),并且退出了上面的while,说明其已经加入了clh,然后在这里尝试通过acquireQueued获得锁,不成功的话,acquireQueued会park当前线程
  //如果acquireQueued返回true,代表当前线程发生过中断,则通过之前的逻辑判断,如果不是THROW_IE,则一定是REINTERRUPT
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

接着继续下面的代码:

if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();

上面的代码是判断当前节点的nextWaiter是否为null,我们阅读过singal的代码可以知道,如果当前节点转移至CLH队列中,那么当前节点的nextWaiter会被赋null,那么这是正常情况。如果在singal之前线程发生了中断,当前线程会在await的while循环跳出,执行上面的这段代码,这时node.nextWaiter并没有赋值为null,但当前节点会在while循环的中断检查时进入CLH队列(checkInterruptWhileWaiting 会调用transferAfterCancelledWait方法中将waitStatus置为0,然后将当前node进入enq,也就是说当前线程await了,但是还没等被signal就中断了,仍然可以进入clh),这时相当于成功转移了,但Condition队列中还保留当前节点的引用,这时需要清理一下,接着就会调用unlinkCancelledWaiters()方法(单链表去除node ws != CONDITION的节点),清除无用的节点。

处理中断状态

上面的interruptMode似乎代码中还没有做处理,接下来就是处理与中断相关的,interruptMode != 0说明发生了中断:

if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);

这里重复一下,interruptMode会有下面三个值:

  • THROW_IE(-1): 在被signalled之前,当前节点的线程被中断了,代表 await 返回的时候,需要抛出 InterruptedException 异常代表
  • REINTERRUPT(1): 在被signalled之后,当前节点的线程被中断了,代表 await 返回的时候,需要重新设置中断状态
  • 0: 未发生中断

接着执行reportInterruptAfterWait(interruptMode)方法,代码如下,逻辑就是上面的逻辑。

/**
 * Throws InterruptedException, reinterrupts current thread, or
 * does nothing, depending on mode.
 */
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();			//如果是signal之前中断,则抛出中断异常给调用方,可以捕获
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();								//否则设置中断标记
}

注意这里的selfInterrupt()是设置中断标志,代码如下:

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

这里的含义是,在signal之前的中断是异常中断,而signal之后的中断只是一个信号,重设标记即可。

参考文章

AQS源码分析
并发与同步、信号量与管程

你可能感兴趣的:(java)